commit d49ecb7c57a26d7098dadc1d2c85c6a416963ae6 Author: Scott Daniels Date: Sun Mar 31 02:24:39 2013 -0500 Initial fork of version 1.8 from Rugged Circuits diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..951c6e7 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,23 @@ +Metadata-Version: 1.0 +Name: gerbmerge +Version: 1.8 +Summary: Merge multiple Gerber/Excellon files +Home-page: http://ruggedcircuits.com/gerbmerge +Author: Rugged Circuits LLC +Author-email: support@ruggedcircuits.com +License: GPL +Description: GerbMerge is a program that combines several Gerber + (i.e., RS274-X) and Excellon files into a single set + of files. This program is useful for combining multiple + printed circuit board layout files into a single job. + + To run the program, invoke the Python interpreter on the + gerbmerge.py file. On Windows, if you installed GerbMerge in + C:/Python24, for example, open a command window (DOS box) + and type: + C:/Python24/gerbmerge.bat + + For more details on installation or running GerbMerge, see the + URL below. + +Platform: all diff --git a/doc/autosearch.html b/doc/autosearch.html new file mode 100644 index 0000000..821c1f3 --- /dev/null +++ b/doc/autosearch.html @@ -0,0 +1,147 @@ + + + + GerbMerge -- A Gerber-file merging program -- Automatic Placement + + + + + +

GerbMerge -- Automatic Placement

+ +
+

Rugged Circuits LLC

+
+ +


+ + + + + + + + + +
Top-Level | The Configuration File | The Layout File | Automatic Placement
 
+ Introduction | Randomized Search | + Exhaustive Search | Multiple Instances | Usage Notes

+ +

Introduction

+ +As an alternative to manual placement, either using the layout file +approach or using the --place-file command-line option, GerbMerge can automatically +try to find the best arrangement of jobs on a panel that minimizes the total panel area. +Using automatic placement can save you time since you don't have to construct and experiment +with a layout file. The tradeoff, however, is that automatic placement may take a long time +to execute, and for panels with many, small jobs, the run time may be prohibitive. On the +other hand, experience suggests that good results can be obtained in just a few minutes, +even when GerbMerge is not allowed to search all possibilities. + +

Randomized Search

+ +

The Basics

+The randomized search approach has GerbMerge repeatedly place jobs randomly on a panel, possibly +rotated. After each placement, GerbMerge evaluates the total area of the panel, and if +it's less than the smallest area encountered so far, the placement is memorized as the +best so far. +

This may not sound like a very efficient approach but experience shows that it can +lead to nearly-optimal results fairly quickly. The reason is that although there can +be a huge number of possible placements for a given set of jobs, many of them are +equivalent with respect to total panel area. +

The randomized search approach is the default automatic placement method. It is +invoked simply by not specifying any layout file: +

gerbmerge file.cfg
+

The configuration file must still be specified, of course. +After GerbMerge starts, you may press Ctrl-C at any +time to stop the process. In fact, you must press Ctrl-C at some point as GerbMerge +will try random placements forever. +

The best layout found when Ctrl-C is pressed will be used for panelization. +Note that the layout is also saved in the file specified by the +Placement assignment in the [MergeOutputFiles] section of the +configuration file. Thus, if you want to experiment, +you can run different trials, save the best placements from each, then use the +best one by using the saved placement file as the input to +GerbMerge with the --place-file option. + +

Random+Exhaustive

+The default operation of GerbMerge is to actually perform a hybrid search, +using both random search and exhaustive search. By default, GerbMerge will take +a list of N jobs and randomly place N-2 of them (randomly chosen). Then, +GerbMerge will exhaustively try to place the remaining 2 jobs on the panel to +minimize the area. This approach has been found to improve panel usage at minimal +cost since an exhaustive search of only 2 jobs is very quick. +

You can change the number of jobs to exhaustively search for a given random +placement with the --rs-fsjobs command-line option. For example, +

gerbmerge --rs-fsjobs=2 file.cfg
+The above example is the default behavior, i.e., exhaustively place 2 jobs and +randomly place N-2 jobs. By using a number higher than 2, there is less randomness +but fewer starting placements are tested per second. + +

Exhaustive Search

+ +The exhaustive search approach has GerbMerge try all possible placements for a given +set of jobs, one by one. This sounds like it may be an exponentially long approach, +and it is. For anything other than a few boards (less than 5 or so), exhaustive search +is prohibitive. +

The exhaustive search mode is invoked as follows: +

gerbmerge --full-search file.cfg
+You can stop the search at any time by pressing Ctrl-C. The best placement found so far +will be used for panelization and saved in the placement file specified by the Placement value in the +[MergeOutputFiles] section of the configuration file. + +

Multiple Instances

+There is no need to repeat sections of a job in the configuration file if you want +a job to appear multiple times on a panel. You can use the Repeat=N configuration +option to indicate that a particular job is to have N copies on a panel. For example: +
  [irtx]
+  Prefix=%(projdir)s/IRTransmitter/irtx
+  *TopLayer=%(prefix)s.cmp
+  *BottomLayer=%(prefix)s.sol
+  Drills=%(prefix)s.xln
+  BoardOutline=%(prefix)s.bor
+  *SolderMaskTop=%(prefix)s.stc
+  *SolderMaskBottom=%(prefix)s.sts
+  Repeat=5
+
+This job specifies all the layers as usual, then the last line indicates that 5 +such jobs are to appear on the final panel. They may appear in various positions +and states of rotation, however. + +

Usage Notes

+ +

Area Estimates

+GerbMerge will estimate and display the maximum possible panel usage as a percentage. +This estimate is frequently too high, as GerbMerge simply takes the area of each +job, adds in the area required by inter-job spacing, then adds all of these areas +together. The amount of that area that is used by actual jobs (and not inter-job +spacing) represents the best possible area usage. This usage will clearly not be +achieved unless all jobs magically fit together perfectly. +

In summary, it is pointless to wait for a random search for hours to hit +an estimated area utilization of 91% because, unless the dimensions of all +boards line up just so, that utilization is not achievable. +

Panel Width and Height

+Note that the PanelWidth and PanelHeight options in the +configuration file constrain the search process. GerbMerge will not allow +any placement, either by random search or exhaustive search, to exceed the +panel dimensions. You can, therefore, guide the search process by choosing a +panel size that is not too large, thus preventing highly-unlikely placements +(think all jobs in one row) from being considered. +

Similarly, by choosing panels that are slightly wider than taller, or vice +versa, different placements can be considered and may lead to different +results. Consider these two configuration file options as a source of +experimentation. +

Time vs. Money

+How long should you wait for the best possible area utilization? It depends... +how much is your time worth? +

If you've achieved 85% utilization for a 30 sq. in. board, what will you +save by waiting and hoping for 90% (i.e., 28.3 sq. in.)? Assuming 64 cents/sq. in. +(BareBonesPCB.com cost), you will save $1.09. +


+ +

© 2003-2011, Copyright by Rugged Circuits LLC; All Rights Reserved. mailto: support@ruggedcircuits.com
+ + + diff --git a/doc/cfgfile.html b/doc/cfgfile.html new file mode 100644 index 0000000..deddad6 --- /dev/null +++ b/doc/cfgfile.html @@ -0,0 +1,441 @@ + + + + GerbMerge -- A Gerber-file merging program -- The Configuration File + + + + + +

GerbMerge -- The Configuration File

+ +
+

Rugged Circuits LLC

+
+ +


+ + + + + + + + + +
Top-Level | The Configuration File | The Layout File | Automatic Placement
 
+ Operating Parameters | Job Descriptions | + Syntax Notes

+ +

Introduction

+ +

The configuration file tells GerbMerge things like: +

    +
  • How much space to leave between jobs in the final panel

  • +
  • Whether or not to draw cut lines or crop marks, and on which layers

  • +
  • Whether or not to generate a fabrication drawing

  • +
  • The job names and files to be panelized

  • +
  • The output file names

  • +
+ +

Note that the configuration file does not specify layout of jobs on the + panel. This layout is described by the layout file. + The layout of jobs on the panel may also be constructed automatically using + the automatic placement mode of operation. + +

Help!

+

The rest of this document has a lot of information and it's easy to get overwhelmed. Users of GerbMerge +complain that the configuration file is the biggest hurdle to overcome in using the program. +

Don't panic. Start with a sample configuration file and +modify it for your own jobs. The comments in the sample file will guide you through the +process. + +

+

Syntax Notes

+ +

The configuration file is a plain text file that can be created with any +text editor. It is parsed using Python's +ConfigParser module. +See the documentation for this module for a full description of supported syntax. + +

Note that comments in this file begin with a '#' character. +Comments must occupy an entire line and must not have any characters before the +'#' character, including blanks. Comments cannot be placed at the end of a +line. For example: +

+        # This is correct...a comment occupies the entire line
+            # Incorrect...comment preceded by blanks
+        PanelWidth = 10.5   # This is INCORRECT...a comment cannot be placed at the end of a line
+
+

The configuration file has a standard "INI-style" syntax comprising: +

    +
  • Sections delimited by section names in square brackets (e.g., [Options]) +
  • Assignments of the form 'Name = Value' +
+

The configuration file parser supports variable substitution. You can specify a common +pathname prefix, for example, and substitute it in subsequent assignments, like this: +

+    [CPUBoard]
+    Prefix = /home/user/eagle/cpuboard
+
+    # Note the syntax '%(prefix)s' is a variable string substitution.
+    # Even though we said 'Prefix = ...' we use LOWERCASE 'prefix' in the actual substitution!
+    BoardOutline = %(prefix)s/cpu.bor
+    Drills       = %(prefix)s/cpu.xln
+
+ +
+

NOTE: the parser converts all names you assign to into lowercase letters only. +


+

In general, assignments are local to the section in which they reside, i.e., +the names assigned to are not visible in the other sections. However, any +assignments in the section named [DEFAULT] are visible in all +sections. For example: +

+    [DEFAULT]
+    EagleDir = /home/user/eagle
+
+    [CPUBoard]
+    Prefix = %(eagledir)s/cpuboard
+    BoardOutline = %(prefix)s/cpu.bor
+    Drills       = %(prefix)s/cpu.xln
+
+    [IOBoard]
+    Prefix = %(eagledir)s/ioboard
+    BoardOutline = %(prefix)s/io.bor
+    Drills       = %(prefix)s/io.xln
+
+ +

Have a look at the sample configuration files layout1.cfg + and layout2.cfg for a quick overview of this file's syntax. + +

+

Operating Parameters

+ +

The first section of the configuration file is called [Options]. +This section specifies operating parameters for the job. +

The following optional parameters are supported: +

+
Tool List
+
ToolList = /home/user/eagle/toollist.drl +

NOTE:If you're using a recent version of Eagle or other modern PCB program, you can probably + ignore this option. Try commenting it out and see what happens! +

This parameter sets the default tool list (or "drill rack") in effect for jobs that (a) do not have + embedded tool sizes in the Excellon file, and (b) do not have a tool list specified + as part of the job description (see below). +

As of Eagle version 4.11r2, tool sizes are embedded in the Excellon file, like this: +

+                    ...
+                    T01C0.032
+                    T02C0.045
+                    T03C0.115
+                    ...
+ 
+

For Excellon files with embedded tool sizes, no tool list file need be specified. Otherwise, + a tool list file must be specified that contains something like: +

+               T01 0.032in
+               T02 0.045in
+               T03 0.115in
+ 
+

Suffixes of 'mm' and 'mil' may be used instead of 'in' to indicate millimetres and mils. +

Note that Eagle's CAM Processor uses two different forms of Excellon output + devices, EXCELLON in which tool sizes are embedded in the drill file, + and EXCELLON_RACK which requires an external tool list file, or drill rack. The + latter may be desirable when you want to send your boards to a manufacturer + with a limited set of drill sizes, or that charges by the number of different + drill sizes used. In this case, do specify the ToolList option and + set it to the drill rack you specified for the EXCELLON_RACK device. +

+ +
Excellon Decimals
+
ExcellonDecimals = 4 +

This optional setting specifies the number of digits after the decimal point in the input + Excellon drill files. These files contain (X,Y) drill locations specified as integers, but + represent actual positions in the format M.N where there are M digits + before the decimal point and N digits after. The default number of decimal digits + is 4, hence a 2.4 integer format, so that the drill instruction X12300Y9400 means to drill at (1.23", 0.94"). + In 2.3 format (with ExcellonDecimals=3) the above would appear as + X1230Y940. Note that it is assumed that leading 0's are omitted, or else no zeroes are omitted. + Omitted trailing 0's are not yet supported. +

As of this writing, older Eagle versions use a 2.3 format (prior to version 4.11r12), more recent Eagle versions use a 2.4 format, while Orcad and PCB use 2.4.

+

Note that each job may have its own ExcellonDecimals setting (see below) to override this global + setting. +

Finally, note that the ExcellonDecimals option applies to the expected format for + the input Excellon files, i.e., the drill files that GerbMerge reads in. The + ExcellonLeadingZeros option below applies to the output Excellon file generated by GerbMerge. + +

Cut Line Layers
+
CutLineLayers = *toplayer,*bottomlayer +

This parameter indicates which, if any, layers are to have cut lines drawn on them. Cut lines + define the rectangular extents of each individual job on the panel. They are intended to help you + in cutting out the individual jobs from the panel. +

The value of this parameter is a list of layer names, which are defined for each job (see below). + Layer names may be separated with commas or semicolons. +

Note that layer names must be written in lowercase letters, even if they are defined with + uppercase letters. Also note that all layer names except the board outline layer will begin + with an asterisk '*'.

+

This parameter may be omitted, or be set to None to indicate that no cut lines should + be drawn. + +

Crop Mark Layers
+
CropMarkLayers = *toplayer,*bottomlayer +

This parameter indicates which, if any, layers are to have crop marks drawn on them. Crop marks + are small L-shaped marks at the four corners of the final panel. Some board manufacturers require + crop marks to ensure registration and to unambiguously define the extents of the job. +

The value of this parameter is a list of layer names, which are defined for each job (see below). + Layer names may be separated with commas or semicolons. +

Note that layer names must be written in lowercase letters, even if they are defined with + uppercase letters. Also note that all layer names except the board outline layer will begin + with an asterisk '*'. +

This parameter may be omitted, or be set to None to indicate that no crop marks should + be drawn.

+ +
Fabrication Drawing File
+
FabricationDrawingFile = fabdwg.ger +

This optional parameter may be set to a filename, to 'none', or omitted + entirely. When a valid filename is specified, GerbMerge will generate a Gerber + RS274X file containing a fabrication drawing for the entire project. The + drawing contains a box for the outline of the entire panel, dimension arrows + for the panel, drill symbols for each drill hit, a drill tool legend, and + optional user text. Some board manufacturers require a fabrication drawing. +

The Fabrication Drawing Text option below allows you add user-defined text + to this drawing. +

Note that for generating a fabrication drawing, no more + than 26 drill tools can appear in the merged output.

+ +
Fabrication Drawing Text
+
FabricationDrawingText = project/fabdwg.txt +

This optional parameter may specify the name of a file containing plain text. Each line + in the file is added to the fabrication drawing, if one is enabled.

+ +
ExcellonLeadingZeros
+
ExcellonLeadingZeros = 0 +

This optional setting creates a merged Excellon output file with leading + zeros. The default is to use leading-zero suppression. For example, with leading-zero + suppression, a drill hit at location (1.23",4.56") would be written in the output Excellon file as: +

        X12300Y45600
+   
Without leading-zero suppression (i.e., with ExcellonLeadingZeros=1) it would appear as: +
        X012300Y045600
+   

Not using leading-zero suppression may make it easier for some + Gerber viewers to properly interpret the Excellon file. Try setting + ExcellonLeadingZeros=1 if your drills appear in completely the + wrong locations when viewing your merged output files in a Gerber + viewer.

+

Finally, note that the ExcellonLeadingZeros option applies to the format for + the output Excellon file as generated by GerbMerge. The + ExcellonDecimals option described above applies to the input Excellon files read + in by GerbMerge. + +

Outline Layer File
+
OutlineLayerFile = project.oln +

This optional parameter indicates that an additional output file (Gerber layer) is to + be generated containing a rectangle that is drawn around the edges of the final panelized + job. The value of this parameter is the name of the file. If 'none' is specified or this option + is omitted, no outline file is generated. +

This outline layer is useful in circuit board milling + for defining the path extents of a contour router bit so that the entire panel may be cut out by + the mill.

+ +
Scoring File
+
ScoringFile = project.sco +

This optional parameter indicates that an additional output file (Gerber layer) is to + be generated containing scoring lines. These scoring lines describe the path for a scoring + tool to make V-grooves in the board in between jobs, so that the jobs may be easily snapped + apart. The value of this parameter is the name of the file. If 'none' is specified or this option + is omitted, no scoring file is generated.

+ +
Panel Width/Height
+
PanelWidth = 12.6
+ PanelHeight = 7.8 +

These parameters (in inches) set the dimensions of the board manufacturer's panels. An error message + will be displayed if the panelized job exceeds these dimensions. You can change these settings + to match the panel size of your board manufacturer, if you know it. +

For automatic placement, the panel size defined by these settings + constraint the random placements such that only placements that would fit on the panel are considered.

+ +
Margins
+
LeftMargin = 0.1
+ RightMargin = 0.1
+ TopMargin = 0.1
+ BottomMargin = 0.1 +

These four parameters 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 an outline layer. + +

Job Spacing
+
XSpacing = 0.125
+ YSpacing = 0.125 +

These parameters set the job-to-job spacing in horizontal (X) and vertical (Y) directions. + The default spacing is 0.125 inches if these parameters are not specified. Normally, both + parameters will have the same value, but different values can be used to "tweak" + a panel to exactly fit some dimensions.

+ +
Cut Line Width
+
CutLineWidth = 0.01 +

This optional parameter (in inches) indicates the width of the line used to draw cut lines. If not + specified, the default is 0.01".

+ +
Crop Mark Width
+
CropMarkWidth = 0.01 +

This optional parameter (in inches) indicates the width of the line used to draw crop marks. If not + specified, the default is 0.01".

+ +
Allow Missing Layers
+
AllowMissingLayers = 0 +

This parameter may be set to either 0 or 1. When set to 0, all jobs must have the same + layer names. This is the most common case. This parameter guards against misspelling of + layer names and having them mistakenly placed on a different layer. +

Some jobs, however, will have fewer or more layers. For example, mixing jobs that do + and do not have surface-mount components may mean that some jobs will have solder mask layers + and some will not. Setting AllowMissingLayers to 1 allows you to panelize such + job mixtures. Take care, however, to inspect the output carefully + in this case to catch layer-name surprises.

+ +
DrillClusterTolerance
+
DrillClusterTolerance = 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 (the default) disables clustering.

+ +
MinimumFeatureSize
+
MinimumFeatureSize = None +

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. Example usage is: +

   MinimumFeatureSize = *topsilkscreen,0.008,*bottomsilkscreen,0.008
+ +
FiducialPoints
+
FiducialPoints = None +

Use this option to automatically add fiducials (little round markers used to aid +in automatic component placement) to your final panel. This makes the most sense when +you leave some margins around your panel and place the fiducials on the margins. +

The parameter to this option is a list of X,Y points at which to draw fiducials, relative to the +edges of the final panel. Positive values are relative to the lower-left, while negative values are +relative to the upper-right. For example: +

   FiducialPoints = 0.125,0.125,-0.1,-0.1
+would place one fiducial at (0.125,0.125) relative to the lower-left, and another fiducial +a distance of (0.1,0.1) from the top-right of the final panel. To place a fiducial at the top-left +of the final panel: +
   FiducialPoints = 0.125,-0.125
+The FiducialCopperDiameter and FiducialMaskDiameter options control the appearance of the fiducials.
+

+

FiducialCopperDiameter
+
FiducialCopperDiameter = 0.08 +

This option sets the diameter of fiducials in inches. See the FiducialPoints configuration option for more information.

+ +
FiducialMaskDiameter
+
FiducialMaskDiameter = 0.32 +

This option sets the diameter of the soldermask opening around fiducials in inches. See the FiducialPoints configuration option for more information.

+ +
+ +

+

Job Descriptions

+ +

Each input job is described in its own section. The job is described by providing file names for +each layer. Layer names are up to you, but note the following: +

    +
  • Layer names may be specified with lowercase and uppercase letters, but are converted to +all-lowercase by GerbMerge. Note that this applies to layer names, not filenames. +

  • Each job must have at least a 'boardoutline' and 'drills' layer, specifying the Gerber +board outline and Excellon drills layer, respectively. +

  • Each job may have an optional 'toollist' file specifying the tool list (or "drill rack") +in effect for this job only. This setting overrides the global ToolList option, described above. +If the Excellon file for the job has embedded tool sizes, this option is ignored. +

  • All layer names other than 'boardoutline' and 'drills' must begin with an asterisk '*' +character. +

+ +

Consider the following example: +

+    [CPUBoard]
+    BoardOutline = /home/user/eagle/cpuboard/cpu.bor
+    Drills       = /home/user/eagle/cpuboard/cpu.xln
+    ToolList     = /home/user/eagle/cpuboard/tools.drl
+    *TopLayer    = /home/user/eagle/cpuboard/cpu.cmp
+    *BottomLayer = /home/user/eagle/cpuboard/cpu.sol
+    *Silkscreen  = /home/user/eagle/cpuboard/cpu.plc
+
+

Job names (in square brackets) are fairly arbitrary and need not correspond to any file names. They must, +however, comprise only letters, digits, and the underscore character. Furthermore, job names must begin with +a letter. Job names, unlike layer names, are case sensitive. +

Each assignment statement assigns a file name to a layer name. As mentioned above, +the layer names 'boardoutline' and 'drills' are reserved and required. The optional 'toollist' layer +is not an actual layer but an assignment that indicates the tool list in effect for this job. All other layer +names are up to you and must begin with an asterisk '*'. +

Make good use of variable substitutions (see the sample layout1.cfg +and layout2.cfg files) to avoid +typing the same pathname over and over. +

In addition to specifying board layers, each job description can also have job-specific +parameter assignments: +

+
Repeat
+
Repeat = 3 +

This option is only used for automatic placement and indicates the number + of times this job is to appear in the final panel. For manual placement, this option is ignored. This option + may be left unspecified in which case a repeat count of 1 is assumed. + +

ExcellonDecimals
+
ExcellonDecimals = 3 +

This option overrides the global ExcellonDecimals setting in the + [Options] section for this job only. This allows jobs with different + Excellon decimal formats to be panelized. This option may be left unspecified + in which case the global ExcellonDecimals setting is applied. +

+ +

+

Merge Output Files

+ +

GerbMerge combines data from multiple jobs grouped by layer. All of the "bottom copper" +layers from all jobs, for example, will be combined into a single "bottom copper" file. +The names of these combined output files can be set in the [MergeOutputFiles] section of the +configuration file. +

This section contains assignments of file names to layer names. The layer names must be the +same as the ones specified in the [Jobs] section of the configuration file. +All layer names must begin with an asterisk '*' except for the following four reserved layer names: +

  • BoardOutline
  • +
  • Drills
  • +
  • Placement
  • +
  • ToolList
  • +
+

The first two reserved layer names are actual layers, while +Placement refers to the placement file generated by GerbMerge +containing positions of jobs on the final panel, and ToolList refers +to the combined tool list file generated by GerbMerge. +

Any assignment made in this section that does not begin with an asterisk or is not an assignment +to one of the above four reserved names is considered a general variable assignment for future +string substitution. +

Here is an example: +

+  [MergeOutputFiles]
+  Prefix = job1
+  BoardOutline = %(prefix)s.bor
+  Drills = %(prefix)s.xln
+  *topcopper = %(prefix)s.cmp
+  *bottomcopper = %(prefix)s.sol
+
+

If an assignment to a layer name is missing, GerbMerge will create the file merged.layername.ger where +'layername' is the layer name. Default values for the four reserved names are merged.boardoutline.ger +for the BoardOutline layer, merged.drills.xln for the Drills layer, merged.placement.txt for the Placement file, and merged.toollist.drl for the ToolList combined tool list file. + +


+ +

© 2003-2011, Copyright by Rugged Circuits LLC; All Rights Reserved. mailto: support@ruggedcircuits.com
+ + + diff --git a/doc/ex1.png b/doc/ex1.png new file mode 100644 index 0000000..67adfcc Binary files /dev/null and b/doc/ex1.png differ diff --git a/doc/ex1a.png b/doc/ex1a.png new file mode 100644 index 0000000..a0bda42 Binary files /dev/null and b/doc/ex1a.png differ diff --git a/doc/ex1b.png b/doc/ex1b.png new file mode 100644 index 0000000..3e48718 Binary files /dev/null and b/doc/ex1b.png differ diff --git a/doc/ex1c.png b/doc/ex1c.png new file mode 100644 index 0000000..cce7632 Binary files /dev/null and b/doc/ex1c.png differ diff --git a/doc/ex1d.png b/doc/ex1d.png new file mode 100644 index 0000000..457f19b Binary files /dev/null and b/doc/ex1d.png differ diff --git a/doc/ex1e.png b/doc/ex1e.png new file mode 100644 index 0000000..25433c7 Binary files /dev/null and b/doc/ex1e.png differ diff --git a/doc/ex1f.png b/doc/ex1f.png new file mode 100644 index 0000000..5e425ed Binary files /dev/null and b/doc/ex1f.png differ diff --git a/doc/ex1g.png b/doc/ex1g.png new file mode 100644 index 0000000..061ad3c Binary files /dev/null and b/doc/ex1g.png differ diff --git a/doc/gpl.html b/doc/gpl.html new file mode 100644 index 0000000..991b592 --- /dev/null +++ b/doc/gpl.html @@ -0,0 +1,696 @@ + + + + + GerbMerge -- License + + + + + +

GerbMerge -- A Gerber-file merging program

+
+

Rugged Circuits LLC

+
+


+
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. 
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    
+    Copyright (C)   
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+      Copyright (C)   
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+.
+
+
+

© 2003-2011, Copyright by Rugged Circuits LLC; All Rights Reserved. mailto: support@ruggedcircuits.com
+ + diff --git a/doc/index.html b/doc/index.html new file mode 100644 index 0000000..24cf1c3 --- /dev/null +++ b/doc/index.html @@ -0,0 +1,392 @@ + + + + + GerbMerge -- A Gerber-file merging program + + + + + +

GerbMerge -- A Gerber-file merging program

+ +
+

Rugged Circuits LLC

+
+ +


+ + + + + + + + + + + + +
Top-Level | The Configuration File | The Layout File | Automatic PlacementVersion 1.8
June 8, 2011
+ Requirements | Installation | + Running GerbMerge | Verifying the Output | Limitations | Program Options | Copyright | To + Do | Credits | History

+ +

What's New

+

In release 1.8: +

    +
  • Released under more recent GPL v3 license
  • +
  • Summary statistics prints out smallest drill tool diameter
  • +
  • Added FiducialPoints, FiducialCopperDiameter, and FiducialMaskDiameter configuration options
  • +
  • Added option to write fiducials to final panel
  • +
  • Scoring lines now go all the way across a panel
  • +
+ +

In release 1.7: +

    +
  • Added a new command-line option --search-timeout to time-limit the automatic placement process.
  • +
  • Added preliminary support for a GUI controller interface.
  • +
+ +

+

Introduction

+ +

GerbMerge is a program for combining (panelizing) the CAM data from multiple printed + circuit board designs into a single set of CAM files. The purpose of + doing so is to submit a single job to a board manufacturer, thereby saving on manufacturing costs. +

GerbMerge currently works with: +

    +
  • CAM data generated by the Eagle circuit board + design program, with "best effort" support for Orcad, Protel, and PCB
  • +
  • Artwork in Gerber RS274-X format
  • +
  • Drill files in Excellon format
  • +
+ Here is one sample and another sample of the program's output. These samples + demonstrate panelizing multiple, different jobs, and also demonstrate board rotation. + +

+

Download

+

Version 1.8

+ + +

+

Requirements

+

GerbMerge is written in pure Python. It + depends upon the following packages for operation: +

+

All of the above packages come with easy installation programs for both Windows, Mac OS X, + and Linux. + +

+

Installation

+ +

First, install all of the packages listed above in the Requirements section. + +

Windows

+

Run the gerbmerge1.8.exe installation program. I will assume +you choose all of the default installation options. The installer +will create and populate the following directories:

+
    +
    +c:\Python24\lib\site-packages\gerbmerge
    +c:\Python24\gerbmerge
    +
    +
+

The above assumes you have Python installed in C:\Python24. The +first directory is where the actual program resides. The second directory +contains the documentation, example files, etc. In the C:\Python24 +directory is a sample batch file GERBMERGE.BAT which shows you how to +run the GerbMerge program. + +

Unix / Mac OS X

+

Extract the gerbmerge1.8.tar.gz file then install as follows:

+
    +python setup.py install   (You may need to be root to install to system directories) +
+

The installer will create and populate the following directories/files:

+
    +
    +/usr/local/lib/python2.4/site-packages/gerbmerge
    +/usr/local/lib/python2.4/gerbmerge
    +/usr/local/bin/gerbmerge
    +
    +
+

The above assumes your Python library directory is as indicated (it may be +elsewhere but the installer should be able to find it, so don't worry about +it). The first directory is where the actual program resides. The second +directory contains the documentation, example files, etc. A sample program for +invoking GerbMerge is installed as /usr/local/bin/gerbmerge...feel free to move +it somewhere else. +

Not all Linux distributions are the same, however. If you have trouble, there is a useful set of instructions from Chetan Bhargava for installing GerbMerge on Ubuntu distributions. + +

+

Running GerbMerge

+ +

Windows

+

Open a DOS box and invoke the Python interpreter on the gerbmerge.py file. +Have a look at GERBMERGE.BAT (and put this on your Path somewhere) for an example. +

c:\python24\python c:\python24\lib\site-packages\gerbmerge\gerbmerge.py
+

Unix / Mac OS X

+

You run GerbMerge by invoking the Python interpreter on the gerbmerge.py +file of the gerbmerge package. For example:

+
python /usr/local/lib/python2.4/site-packages/gerbmerge/gerbmerge.py
+

The gerbmerge shell script that comes with this software contains an +example for running GerbMerge, modelled on the above. By default, this shell +script is installed in /usr/local/bin so you should just be able +to type gerbmerge from a shell prompt. +

Operation

+There are three ways to run GerbMerge: +
  1. By manually specifying the relative placement of jobs
  2. +
  3. By manually specifying the absolute placement of jobs
  4. +
  5. By letting GerbMerge automatically search for a placement that minimizes total panel area
  6. +
+

Manual Relative Placement

+For the manual relative placement approach, GerbMerge needs two input text files: +
    +
  • The configuration file specifies global options and defines the jobs +to be panelized

  • +
  • The layout file specifies how the jobs are to be laid out.

  • +
+

The names of these files are the two required parameters to GerbMerge: +

gerbmerge file.cfg file.def
+

The following links describe the contents of the configuration +file and layout file. +

Manual Absolute Placement

+

For the manual absolute placement approach, GerbMerge also needs the configuration file +as well as another text file that specifies where each job is located on the panel and +whether or not it is rotated: +

gerbmerge --place-file=place.txt file.cfg
+

The place.txt file looks something like: +

job1 0.100 0.100
+cpu 0.756 0.100
+cpu*rotated 1.35 1.50
+
+

This method of placement is not meant for normal use. It can be used to recreate +a previous invocation of GerbMerge, since GerbMerge saves its results in a text file +(whose name is set in the [MergeOutputFiles] +section of the configuration file) after every run. Thus, you can experiment with +different parameters, save a placement you like, do some more experimentation, then return +to the saved placement if necessary. +

Alternatively, this method of placement can be used with third-party back ends that +implement intelligent auto-placement algorithms, using GerbMerge only for doing the +actual panelization. +

Automatic Placement

+

For the automatic placement approach, GerbMerge only needs the configuration file: +

gerbmerge file.cfg
+Command-line options can be used to modify the search algorithm. See the +Automatic Placement page for more information. +

Input File Requirements

+GerbMerge requires the following input CAM files: +
    +
  • Each job must have a Gerber file describing the board outline, which is assumed +rectangular. In Eagle, a board outline is usually generated from the Dimension layer. +This board outline is a width-0 line describing the physical extents of the board. If you're +not using Eagle, you don't have to generate a width-0 rectangle, but GerbMerge does need +to use some Gerber layer to determine the extents of the board. GerbMerge will take the maximum +extents of all drawn objects in this layer as the extents of the board.

  • +
  • Each job must have an Excellon drill file.

  • +
  • Each job can have any number of optional Gerber files describing copper +layers, silkscreen, solder masks, etc.

  • +
  • All files must have the same offset and must be shown looking from the +top of the board, i.e., not mirrored.

  • +
  • Each job may have an optional tool list file indicating the tool names +used in the Excellon file and the diameter of each tool. This file is not necessary +if tool sizes are embedded in the Excellon file. A typical tool list file looks like: +

    +          T01 0.025in
    +          T02 0.032in
    +          T03 0.045in
    +
    +
+ +

+

Verifying the Output

+ +

Before sending your job to be manufactured, it is imperative that you verify +the correctness of the output. Remember that GerbMerge comes with NO WARRANTY. +Manufacturing circuit boards costs real money and a single mistake can render +an entire lot of boards unusable. +

I recommend the following programs for viewing the final output data. Take +the time to become very familiar with at least one of these tools and to use +it before every job you send out for manufacture. +

+
gerbv
+
For Linux, the best option (currently) for viewing Gerber and Excellon files +is the gerbv program. Simply +type in the names of all files generated by GerbMerge as parameters to gerbv: +
gerbv merged.*.ger merged.*.xln
+
GC-Prevue
+

For Windows, GC-Prevue is a good program +that I have used often. It is a free program. GraphiCode makes lots of other, more +powerful Gerber manipulation and viewing programs but they are quite pricey ($495 and up).

+
ViewMate
+

Another free Windows program, ViewMate is similar +to GC-Prevue. I have not used ViewMate much, but that is mostly due to familiarity with +GC-Prevue. The two programs are comparable, although I'm sure that someone who is much +more familiar with both could point out some differences.

+
+ +

+

Limitations

+ +
    +
  • This program has mainly been tested with output from the Eagle CAD program. +Limited testing has been performed with Orcad, Protel, and PCB. +Other CAD programs will NOT WORK with a very high probability, as the input +parser is quite primitive. +

    If you have the need/motivation to adapt GerbMerge to other CAD programs, +have a look at the gerber2pdf program. It is written in Python and +implements a much more complete RS274-X input file parser. Combining GerbMerge +with gerber2pdf should be a fairly simple exercise. Also, feel free to +send us samples of Gerber/Excellon output of your CAD tool and we'll see if we can +add support for it. +

  • This program handles apertures that are rectangles, ovals, circles, macros +without parameters or operators, and Eagle octagons (which are defined using a macro with a single parameter, hence currently handled as a special case). +

  • The panelizing capabilities of this program do not allow for arbitrary +placement of jobs, although there is a fair amount of flexibility. +

  • All jobs are assumed to be rectangular in shape. Non-rectangular jobs +can be handled but will lead to wasted space in the final panel. +

  • A maximum of 26 different drill sizes is supported for generating a +fabrication drawing.

  • +
+ +

+

Program Options

+ +
+
--octagons=normal
+
--octagons=rotate
+
The --octagons option affects how the octagon aperture is defined in the output files. The parameter + to this option must either be rotate or normal. Normally, + octagons begin at an angle of 22.5 degrees, but some Gerber viewers have a problem + with that (notably CircuitMaker from LPKF). These programs expect octagons to begin + at 0.0 degrees. +

The --octagons=normal option is the default (22.5 degrees) and need not + be specified. A rotation of 0.0 degrees can be achieved by specifying --octagons=rotate.

+ +

--random-search
+
This option is the default when only a configuration file is specified (see the documentation on Automatic Placement for more information). It indicates that a randomized search of possible job tilings is + to be performed. This option does not make sense when a layout file is specified.
+ +

--full-search
+
This option may be specified to indicate that all possible job tilings are to be searched (see the documentation on Automatic Placement for more information). This option does not make sense when a layout file + is specified.
+ +

--rs-fsjobs=N
+
This option is used with randomized search to indicate how many jobs are to undergo full search for each tiling. See the documentation on Automatic Placement for more information.
+ +

--place-file=filename
+
This option performs a panel layout based upon absolute job positions in + the given text file, rather than by random/full search or by a layout file. + The placement file created by GerbMerge can be used as an input file to + this option in order to recreate a previous layout.
+ +

--no-trim-gerber
+
This option prevents GerbMerge from trying to trim all Gerber data to lie within the + extents of a given job's board outline. Normally, GerbMerge will try to do so to prevent + one job's Gerber data (most notably, silkscreen lines for connectors that protrude from + the board) from interfering with a neighboring job on the final panel. Specify this + command-line option if you do not want this trimming to occur.
+ +

--no-trim-excellon
+
This option prevents GerbMerge from trying to trim all Excellon data to lie within the + extents of a given job's board outline. Normally, GerbMerge will try to do so to prevent + one job's drill holes from landing in the middle of a neighboring job on the final panel. Specify + this command-line option if you do not want this trimming to occur.
+ +

--search-timeout=seconds
+
When random placements are used, this option can be used to automatically terminate the + search process after the specified number of seconds. If the number of seconds is 0 or this + option is not specified, then random placements are tried forever, until Ctrl-C is pressed + to stop the process and keep the best placement so far.
+ +

-h, --help
+
The '-h' or '--help' option prints a brief summary of available options. + +

-v, --version
+
The '-v' or '--version' option prints the current program version and author contact information.
+
+ +

+

Copyright & License

+ +

Copyright © 2011 Rugged Circuits LLC. All Rights Reserved. + mailto: support@ruggedcircuits.com +

GerbMerge comes with ABSOLUTELY NO WARRANTY. This + is free software licensed under the terms of the GNU General + Public License Version 3. You are welcome to redistribute this software + under certain conditions. For more details, see the previous link or + visit The Free Software Foundation. + +

+

To Do

+ +
    +
  1. Accept outputs from more CAD programs
  2. +
  3. A graphical interface for interactive placement
  4. +
  5. Better reporting of parse errors in the layout and configuration files
  6. +
  7. Implement simple primitive for panelizing a single job in an array
  8. +
  9. More intelligent placement algorithms, possibly based on the fabric cutting problem.
  10. +
  11. Accept aperture macro parameters and operators +
+ +

+ +

Credits

+

Thanks to Jace Browning for major contributions to this code. This help file is based on a template for the help file for mxTools + by M.A. Lemburg. + This software was created with VIM; + thanks to the authors of this program and special thanks for + the Python syntax support. Thanks to M.A. Lemburg for his + mxBase + package, Mike Fletcher for his + SimpleParse package, and + the authors of gerbv, a great + Gerber file viewer for Linux/Mac OS X, and, of course, to the + Python developers and + support community.

+

Thanks to Joe Pighetti for making me start writing this program, and to + the Grand Valley State University Firefighting Robot Team for making me finish it.

+

Thanks to Matt Kavalauskas for identifying Eagle's annulus and thermal macros and supporting + the development of the aperture macro code.

+

Thanks to Bohdan Zograf for the Belorussian translation of this documentation.

+ +
+ +

© 2003-2011, Copyright by Rugged Circuits LLC; All Rights Reserved. mailto: support@ruggedcircuits.com
+ + + diff --git a/doc/layout1.cfg b/doc/layout1.cfg new file mode 100644 index 0000000..d38bb0d --- /dev/null +++ b/doc/layout1.cfg @@ -0,0 +1,272 @@ +# 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 = merge1 + +############################################################################# +# 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. +PanelWidth = 12.6 +PanelHeight = 7.8 + +# 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.1 +RightMargin = 0.1 +TopMargin = 0.1 +BottomMargin = 0.1 + +################################################################ +# +# 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.125 +YSpacing = 0.125 + +# 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.cmp +*BottomLayer=%(prefix)s.sol +*TopSilkscreen=%(prefix)s.plc +*BottomSilkscreen=%(prefix)s.pls +*TopSoldermask=%(prefix)s.stc +*BottomSoldermask=%(prefix)s.sts +Drills=%(prefix)s.xln +BoardOutline=%(prefix)s.bor +ToolList = toollist.%(prefix)s.drl +Placement = placement.%(prefix)s.txt + +############################################################################## +# 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/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.cmp +*BottomLayer=%(prefix)s.sol +*TopSilkscreen=%(prefix)s.plc +*BottomSilkscreen=%(prefix)s.pls +*TopSoldermask=%(prefix)s.stc +*BottomSoldermask=%(prefix)s.sts +Drills=%(prefix)s.xln +BoardOutline=%(prefix)s.bor + +# 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 diff --git a/doc/layout1.def b/doc/layout1.def new file mode 100644 index 0000000..7e9dd5d --- /dev/null +++ b/doc/layout1.def @@ -0,0 +1,19 @@ +# This example simply takes the small Proj1 board and panelizes +# it in a 2x4 array. To demonstrate rotation, the second column +# consists of rotated jobs. You wouldn't really do it this way, +# of course, as it wastes space. +Row { + Col { + Proj1 + Proj1 + Proj1 + Proj1 + } + Col { + Proj1 Rotate + Proj1 Rotate + Proj1 Rotate + Proj1 Rotate + } +} + diff --git a/doc/layout2.cfg b/doc/layout2.cfg new file mode 100644 index 0000000..2eb980e --- /dev/null +++ b/doc/layout2.cfg @@ -0,0 +1,282 @@ +# This configuration file demonstrates panelizing multiple, different jobs. +# We panelize the HEXAPOD job and several copies of the Proj1 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 = merge2 + +############################################################################# +# 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. +PanelWidth = 12.6 +PanelHeight = 7.8 + +# 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 +# an OutlineLayer. +LeftMargin = 0.1 +RightMargin = 0.1 +TopMargin = 0.1 +BottomMargin = 0.1 + +################################################################ +# +# 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.125 +YSpacing = 0.125 + +# 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 = 1 + +# 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.cmp +*BottomLayer=%(prefix)s.sol +*TopSilkscreen=%(prefix)s.plc +*BottomSilkscreen=%(prefix)s.pls +*TopSoldermask=%(prefix)s.stc +*BottomSoldermask=%(prefix)s.sts +Drills=%(prefix)s.xln +BoardOutline=%(prefix)s.bor +ToolList = toollist.%(prefix)s.drl +Placement = placement.%(prefix)s.txt + +############################################################################## +# 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/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.cmp +*BottomLayer=%(prefix)s.sol +*TopSilkscreen=%(prefix)s.plc +*BottomSilkscreen=%(prefix)s.pls +*TopSoldermask=%(prefix)s.stc +*BottomSoldermask=%(prefix)s.sts +Drills=%(prefix)s.xln +BoardOutline=%(prefix)s.bor + +# 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 = 11 + +[Hexapod] +Prefix=%(projdir)s/hexapod +*TopLayer=%(prefix)s.cmp +*BottomLayer=%(prefix)s.sol +*TopSilkscreen=%(prefix)s.plc +Drills=%(prefix)s.xln +BoardOutline=%(prefix)s.bor diff --git a/doc/layout2.def b/doc/layout2.def new file mode 100644 index 0000000..251cba5 --- /dev/null +++ b/doc/layout2.def @@ -0,0 +1,57 @@ +# This layout merges a Hexapod and Proj1 boards into a single +# panel. The layout demonstrates nested rows and columns. The +# final arrangement looks like this (make sure you are looking +# at this document with a fixed-width font like Courier): +# +# +-----------------------------------------------------+ +# | Proj1 | Proj1 | Proj1 | Proj1 | Proj1 | | +# | | | | | | | +# | | | | | | | +# +---------+---------+---------+---------+--------+ | +# | | +# | +-------+-------+ +# | | P | P | +# +--------------------------------+ | r | r | +# | | | o | o | +# | | | j | j | +# | | | 1 | 1 | +# | | +-------+-------+ +# | | | P | P | +# | | | r | r | +# | | | o | o | +# | Hexapod | | j | j | +# | | | 1 | 1 | +# | | +-------+-------+ +# | | | P | P | +# | | | r | r | +# | | | o | o | +# | | | j | j | +# | | | 1 | 1 | +# +--------------------------------+----+-------+-------+ + +Row { // First row has the hexapod and 2x3 panel of + // rotated Proj1 jobs. + Hexapod + Col { // Could also write this as two separate 1x3 columns + Row { // First 1x2 row + Proj1 Rotate + Proj1 Rotate + } + Row { // Second 1x2 row above first one + Proj1 Rotate + Proj1 Rotate + } + Row { // Third 1x2 row above second row + Proj1 Rotate + Proj1 Rotate + } + } // end of column +} // end of first row + +Row { // Second row has 5x1 panel of Proj1 + Proj1 + Proj1 + Proj1 + Proj1 + Proj1 +} diff --git a/doc/layoutfile.html b/doc/layoutfile.html new file mode 100644 index 0000000..761905e --- /dev/null +++ b/doc/layoutfile.html @@ -0,0 +1,216 @@ + + + + GerbMerge -- A Gerber-file merging program -- The Layout File + + + + + +

GerbMerge -- The Layout File

+ +
+

Rugged Circuits LLC

+
+ +


+ + + +
Top-Level | The Configuration File | The Layout File | Automatic Placement

+ +

Introduction

+ +

The layout file tells GerbMerge how to replicate and/or arrange the jobs that + you specified in the configuration file. The layout + file must be specified when using manual relative placement. See the Automatic + Placement page for an alternative to using the layout file approach. + +

The layout file is a plain text file that can be created with any + text editor. + +

Have a look at the sample layout files layout1.def + and layout2.def for a quick overview of this file. + +

+

Rows and Columns

+ +

The panel layout is specified in terms of cells. Each cell is part of either a +row or column of cells. Each row or column can itself be a part of a column or row, +respectively. In this way, a large variety of layouts can be specified. Unfortunately, +this scheme is fairly easy to implement in code, but it does not allow for arbitrary +placement of jobs. + +

At the top level, you specify the layout of the final panel by specifying the +contents of each row, from left to right. Let's begin with an example. The input job, named example is as follows: +

+We will place three copies of this job, all in a row, using the following layout: +
+  Row {
+    example        # Jobs are listed in a row from left to right
+    example
+    example
+  }
+
+The above layout file leads to the following panel: +
+

The Row { .... } construct indicates a single row of the layout. While you +can add spaces and comments as you please, the word Row and its associated +open-bracket must appear on one line, each job name on a separate line, and the closing +bracket on its own line. Thus, the following is illegal: +

+   Row { example example example }    # Illegal!
+
+ +

The word Rotate following a job indicates that the given instance of the job +is to be rotated by 90 degrees at its current position. For example: +

+  Row {
+    example
+    example Rotate
+    example
+  }
+
+The above layout file leads to the following panel: +
+ +

Rows stack vertically beginning at the bottom of the panel and moving up. For example: +

+  Row {
+    example
+    example
+    example
+  }
+  Row {
+    example
+    example
+    example
+  }
+
+The above layout file leads to the following panel: +
+ +

Suppose now that we want the two jobs on the right to be rotated so the final panel has +a smaller width, but larger height. We can try the following: +

+  Row {
+    example
+    example
+    example Rotate
+  }
+  Row {
+    example
+    example
+    example Rotate
+  }
+
+The above layout file leads to the following panel: +
+ +

This layout is quite wasteful and not quite what we intended. The problem is that +GerbMerge stacks rows on top of each other based upon the highest job within a row. The +height of the first (bottom-most) row, then, is the height of the rotated job. +

What we really want is to +think of our layout in terms of columns (in this case). The first column should be two +jobs stacked on top of each other. The second column should be the same. While the third +column should be two jobs side by side. We can accomplish this effect by placing columns +within a single row. Within a column, jobs are listed in stacking order from bottom to top. +For example: +

+  Row {
+    Col {
+      example
+      example
+    }
+    Col {
+      example
+      example
+    }
+    example Rotate
+    example Rotate
+  }
+
+The above layout file leads to the following panel: +
+ +

Study that layout file carefully. The panel has only a single row with 4 elements. +The first element is a column with two jobs. The second element (immediately to the +right of the first element) is also a column with two jobs. The third element is +a rotated job. The fourth and right-most element is a rotated job. + +

In summary, a row is a list of cells that are laid out from left to right. A cell may +be a simple job, or it may be a column of jobs. The column is treated as a single row cell. + +

Now, let's get fancy and embed a row within a column, like this: +

+  Row {
+    Col {
+      example
+      Row {              # This row sits above a job, in a column.
+        example Rotate   # These are laid out left to right in the
+        example Rotate   # middle of a column.
+      }
+    }
+    example Rotate       # These continue left to right in the main row
+    example Rotate
+  }
+
+The above layout file leads to the following panel: +
+ +

In words, the job consists of a single row. The first cell in the row is a column. +The first cell in the column is a job. Above this cell is another row, which has two +cells (rotated jobs) laid out left-to-right. + +

Keep re-reading the above until it makes sense! + +

Here's a quiz: how can you modify the layout file so that a non-rotated job is +added in the blank space at the top-right of the above panel? Think before proceeding. + +

To add a job on top of the two rotated jobs at the right of the panel, we must +convert those two jobs into a column, like this: +

+  Row {
+    Col {
+      example
+      Row {              # This row sits above a job, in a column.
+        example Rotate   # These are laid out left to right in the
+        example Rotate   # middle of a column.
+      }
+    }
+    Col {
+      Row {
+        example Rotate
+        example Rotate
+      }
+      example            # This job sits on top of the two rotated jobs
+    }
+  }
+
+The above layout file leads to the following panel: +
+ +

Once you get the hang of thinking in terms of recursive rows and columns, the +process is not all that difficult. There is one important rule to remember, however: +

Columns can only be defined within a row, and rows can only be defined within +a column.
+ +

Make sure you have a look at the sample layout files layout1.def +and layout2.def for more examples. + +

Rotation Angles

+

For rotating jobs, most users will simply want to rotate jobs by 90 degrees (counterclockwise). This is achieved using the Rotate keyword as described above. It is also possible to rotate jobs by 180 and 270 degrees for special applications, for example, when panelized jobs are not completely separate but need to interact with each other. +

The full list of rotation keywords recognized in the layout file is as follows: +

    +
  • Rotate, Rotate90: rotate by 90 degrees counterclockwise
  • +
  • Rotate180: rotate by 180 degrees
  • +
  • Rotate270: rotate by 270 degrees counterclockwise
  • +
+ +
+

© 2003-2011, Copyright by Rugged Circuits LLC; All Rights Reserved. mailto: support@ruggedcircuits.com
+ + + diff --git a/doc/sample.jpg b/doc/sample.jpg new file mode 100644 index 0000000..bf883a3 Binary files /dev/null and b/doc/sample.jpg differ diff --git a/doc/sample2.jpg b/doc/sample2.jpg new file mode 100644 index 0000000..e7f4209 Binary files /dev/null and b/doc/sample2.jpg differ diff --git a/gerbmerge/__init__.py b/gerbmerge/__init__.py new file mode 100644 index 0000000..9097b9a --- /dev/null +++ b/gerbmerge/__init__.py @@ -0,0 +1 @@ +# Placeholder for GerbMerge package diff --git a/gerbmerge/amacro.py b/gerbmerge/amacro.py new file mode 100644 index 0000000..79f228d --- /dev/null +++ b/gerbmerge/amacro.py @@ -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 diff --git a/gerbmerge/aptable.py b/gerbmerge/aptable.py new file mode 100644 index 0000000..daacd8c --- /dev/null +++ b/gerbmerge/aptable.py @@ -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] diff --git a/gerbmerge/config.py b/gerbmerge/config.py new file mode 100644 index 0000000..0bf3eb5 --- /dev/null +++ b/gerbmerge/config.py @@ -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)) diff --git a/gerbmerge/drillcluster.py b/gerbmerge/drillcluster.py new file mode 100644 index 0000000..cf9b15a --- /dev/null +++ b/gerbmerge/drillcluster.py @@ -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) diff --git a/gerbmerge/fabdrawing.py b/gerbmerge/fabdrawing.py new file mode 100644 index 0000000..ea2d2bb --- /dev/null +++ b/gerbmerge/fabdrawing.py @@ -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) diff --git a/gerbmerge/geometry.py b/gerbmerge/geometry.py new file mode 100644 index 0000000..4d20d50 --- /dev/null +++ b/gerbmerge/geometry.py @@ -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) 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' diff --git a/gerbmerge/gerbmerge.py b/gerbmerge/gerbmerge.py new file mode 100644 index 0000000..149baca --- /dev/null +++ b/gerbmerge/gerbmerge.py @@ -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 * +* - ViewMate * +* * +* Linux: * +* - gerbv * +* * +* 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 diff --git a/gerbmerge/jobs.py b/gerbmerge/jobs.py new file mode 100644 index 0000000..666afaf --- /dev/null +++ b/gerbmerge/jobs.py @@ -0,0 +1,1288 @@ +#!/usr/bin/env python +""" +This module reads all Gerber and Excellon files and stores the +data for each 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 re +import string +import __builtin__ +import copy +import types + +import aptable +import config +import makestroke +import amacro +import geometry +import util + +# Parsing Gerber/Excellon files is currently very brittle. A more robust +# RS274X/Excellon parser would be a good idea and allow this program to work +# robustly with more than just Eagle CAM files. + +# Reminder to self: +# +# D01 -- move and draw line with exposure on +# D02 -- move with exposure off +# D03 -- flash aperture + +# Patterns for Gerber RS274X file interpretation +apdef_pat = re.compile(r'^%AD(D\d+)([^*$]+)\*%$') # Aperture definition +apmdef_pat = re.compile(r'^%AM([^*$]+)\*$') # Aperture macro definition +comment_pat = re.compile(r'G0?4[^*]*\*') # Comment (GerbTool comment omits the 0) +tool_pat = re.compile(r'(D\d+)\*') # Aperture selection +gcode_pat = re.compile(r'G(\d{1,2})\*?') # G-codes +drawXY_pat = re.compile(r'X([+-]?\d+)Y([+-]?\d+)D0?([123])\*') # Drawing command +drawX_pat = re.compile(r'X([+-]?\d+)D0?([123])\*') # Drawing command, Y is implied +drawY_pat = re.compile(r'Y([+-]?\d+)D0?([123])\*') # Drawing command, X is implied +format_pat = re.compile(r'%FS(L|T)?(A|I)(N\d+)?(X\d\d)(Y\d\d)\*%') # Format statement +layerpol_pat = re.compile(r'^%LP[CD]\*%') # Layer polarity (D=dark, C=clear) + +# Circular interpolation drawing commands (from Protel) +cdrawXY_pat = re.compile(r'X([+-]?\d+)Y([+-]?\d+)I([+-]?\d+)J([+-]?\d+)D0?([123])\*') +cdrawX_pat = re.compile(r'X([+-]?\d+)I([+-]?\d+)J([+-]?\d+)D0?([123])\*') # Y is implied +cdrawY_pat = re.compile(r'Y([+-]?\d+)I([+-]?\d+)J([+-]?\d+)D0?([123])\*') # X is implied + +IgnoreList = ( \ + # These are for Eagle, and RS274X files in general + re.compile(r'^%OFA0B0\*%$'), + re.compile(r'^%IPPOS\*%'), + re.compile(r'^%AMOC8\*$'), # Eagle's octagon defined by macro with a $1 parameter + re.compile(r'^5,1,8,0,0,1\.08239X\$1,22\.5\*$'), # Eagle's octagon, 22.5 degree rotation + re.compile(r'^5,1,8,0,0,1\.08239X\$1,0\.0\*$'), # Eagle's octagon, 0.0 degree rotation + re.compile(r'^\*?%$'), + re.compile(r'^M0?2\*$'), + + # These additional ones are for Orcad Layout, PCB, Protel, etc. + re.compile(r'\*'), # Empty statement + re.compile(r'^%IN.*\*%'), + re.compile(r'^%ICAS\*%'), # Not in RS274X spec. + re.compile(r'^%MOIN\*%'), + re.compile(r'^%ASAXBY\*%'), + re.compile(r'^%AD\*%'), # GerbTool empty aperture definition + re.compile(r'^%LN.*\*%') # Layer name + ) + +# Patterns for Excellon interpretation +xtool_pat = re.compile(r'^(T\d+)$') # Tool selection +xydraw_pat = re.compile(r'^X([+-]?\d+)Y([+-]?\d+)$') # Plunge command +xdraw_pat = re.compile(r'^X([+-]?\d+)$') # Plunge command, repeat last Y value +ydraw_pat = re.compile(r'^Y([+-]?\d+)$') # Plunge command, repeat last X value +xtdef_pat = re.compile(r'^(T\d+)(?:F\d+)?(?:S\d+)?C([0-9.]+)$') # Tool+diameter definition with optional + # feed/speed (for Protel) +xtdef2_pat = re.compile(r'^(T\d+)C([0-9.]+)(?:F\d+)?(?:S\d+)?$') # Tool+diameter definition with optional + # feed/speed at the end (for OrCAD) +xzsup_pat = re.compile(r'^INCH,([LT])Z$') # Leading/trailing zeros INCLUDED + +XIgnoreList = ( \ + re.compile(r'^%$'), + re.compile(r'^M30$'), # End of job + re.compile(r'^M48$'), # Program header to first % + re.compile(r'^M72$') # Inches + ) + +# A Job is a single input board. It is expected to have: +# - a board outline file in RS274X format +# - several (at least one) Gerber files in RS274X format +# - a drill file in Excellon format +# +# The board outline and Excellon filenames must be given separately. +# The board outline file determines the extents of the job. + +class Job: + def __init__(self, name): + self.name = name + + # Minimum and maximum (X,Y) absolute co-ordinates encountered + # in GERBER data only (not Excellon). Note that coordinates + # are stored in hundred-thousandsths of an inch so 9999999 is 99.99999 + # inches. + self.maxx = self.maxy = -9999999 # in the case all coordinates are < 0, this will prevent maxx and maxy from defaulting to 0 + self.minx = self.miny = 9999999 + + # Aperture translation table relative to GAT. This dictionary + # has as each key a layer name for the job. Each key's value + # is itself a dictionary where each key is an aperture in the file. + # The value is the key in the GAT. Example: + # apxlat['TopCopper']['D10'] = 'D12' + # apxlat['TopCopper']['D11'] = 'D15' + # apxlat['BottomCopper']['D10'] = 'D15' + self.apxlat = {} + + # Aperture macro translation table relative to GAMT. This dictionary + # has as each key a layer name for the job. Each key's value + # is itself a dictionary where each key is an aperture macro name in the file. + # The value is the key in the GAMT. Example: + # apxlat['TopCopper']['THD10X'] = 'M1' + # apxlat['BottomCopper']['AND10'] = 'M5' + self.apmxlat = {} + + # Commands are one of: + # A. strings for: + # - aperture changes like "D12" + # - G-code commands like "G36" + # - RS-274X commands like "%LPD*%" that begin with '%' + # B. (X,Y,D) triples comprising X,Y integers in the range 0 through 999999 + # and draw commands that are either D01, D02, or D03. The character + # D in the triple above is the integer 1, 2, or 3. + # C. (X,Y,I,J,D,s) 6-tuples comprising X,Y,I,J integers in the range 0 through 999999 + # and D as with (X,Y,D) triples. The 's' integer is non-zero to indicate that + # the (I,J) tuple is a SIGNED offset (for multi-quadrant circular interpolation) + # else the tuple is unsigned. + # + # This variable is, as for apxlat, a dictionary keyed by layer name. + self.commands = {} + + # This dictionary stores all GLOBAL apertures actually needed by this + # layer, i.e., apertures specified prior to draw commands. The dictionary + # is indexed by layer name, and each dictionary entry is a list of aperture + # code strings, like 'D12'. This dictionary helps us to figure out the + # minimum number of apertures that need to be written out in the Gerber + # header of the merged file. Once again, the list of apertures refers to + # GLOBAL aperture codes in the GAT, not ones local to this layer. + self.apertures = {} + + # Excellon commands are grouped by tool number in a dictionary. + # This is to help sorting all jobs and writing out all plunge + # commands for a single tool. + # + # The key to this dictionary is the full tool name, e.g., T03 + # as a string. Each command is an (X,Y) integer tuple. + self.xcommands = {} + + # This is a dictionary mapping LOCAL tool names (e.g., T03) to diameters + # in inches for THIS JOB. This dictionary will be initially empty + # for old-style Excellon files with no embedded tool sizes. The + # main program will construct this dictionary from the global tool + # table in this case, once all jobs have been read in. + self.xdiam = {} + + # This is a mapping from tool name to diameter for THIS JOB + self.ToolList = None + + # How many times to replicate this job if using auto-placement + self.Repeat = 1 + + # How many decimal digits of precision there are in the Excellon file. + # A value greater than 0 overrides the global ExcellonDecimals setting + # for this file, allowing jobs with different Excellon decimal settings + # to be combined. + self.ExcellonDecimals = 0 # 0 means global value prevails + + def width_in(self): + "Return width in INCHES" + return float(self.maxx-self.minx)*0.00001 + + def height_in(self): + "Return height in INCHES" + return float(self.maxy-self.miny)*0.00001 + + def jobarea(self): + return self.width_in()*self.height_in() + + def maxdimension(self): + return max(self.width_in(),self.height_in()) + + def mincoordinates(self): + "Return minimum X and Y coordinate" + + return self.minx, self.miny + + def fixcoordinates(self, x_shift, y_shift): + "Add x_shift and y_shift to all coordinates in the job" + + # Shift maximum and minimum coordinates + self.minx += x_shift + self.maxx += x_shift + self.miny += y_shift + self.maxy += y_shift + + # Shift all commands + for layer, command in self.commands.iteritems(): + + # Loop through each command in each layer + for index in range( len(command) ): + c = command[index] + + # Shift X and Y coordinate of command + if type(c) == types.TupleType: ## ensure that command is of type tuple + command_list = list(c) ## convert tuple to list + if (type( command_list[0] ) == types.IntType) \ + and (type( command_list[1] ) == types.IntType): ## ensure that first two elemenst are integers + command_list[0] += x_shift + command_list[1] += y_shift + command[index] = tuple(command_list) ## convert list back to tuple + + self.commands[layer] = command ## set modified command + + # Shift all excellon commands + for tool, command in self.xcommands.iteritems(): + + # Loop through each command in each layer + for index in range( len(command) ): + c = command[index] + + # Shift X and Y coordinate of command + command_list = list(c) ## convert tuple to list + if ( type( command_list[0] ) == types.IntType ) \ + and ( type( command_list[1] ) == types.IntType ): ## ensure that first two elemenst are integers + command_list[0] += x_shift / 10 + command_list[1] += y_shift / 10 + command[index] = tuple(command_list) ## convert list back to tuple + + self.xcommands[tool] = command ## set modified command + + def parseGerber(self, fullname, layername, updateExtents = 0): + """Do the dirty work. Read the Gerber file given the + global aperture table GAT and global aperture macro table GAMT""" + + GAT = config.GAT + GAMT = config.GAMT + # First construct reverse GAT/GAMT, mapping definition to code + RevGAT = config.buildRevDict(GAT) # RevGAT[hash] = aperturename + RevGAMT = config.buildRevDict(GAMT) # RevGAMT[hash] = aperturemacroname + + #print 'Reading data from %s ...' % fullname + + fid = file(fullname, 'rt') + currtool = None + + self.apxlat[layername] = {} + self.apmxlat[layername] = {} + self.commands[layername] = [] + self.apertures[layername] = [] + + # These divisors are used to scale (X,Y) co-ordinates. We store + # everything as integers in hundred-thousandths of an inch (i.e., M.5 + # format). If we get something in M.4 format, we must multiply by + # 10. If we get something in M.6 format we must divide by 10, etc. + x_div = 1.0 + y_div = 1.0 + + # Drawing commands can be repeated with X or Y omitted if they are + # the same as before. These variables store the last X/Y value as + # integers in hundred-thousandths of an inch. + last_x = last_y = 0 + + # Last modal G-code. Some G-codes introduce "modes", such as circular interpolation + # mode, and we want to remember what mode we're in. We're interested in: + # G01 -- linear interpolation, cancels all circular interpolation modes + # G36 -- Turn on polygon area fill + # G37 -- Turn off polygon area fill + last_gmode = 1 # G01 by default, linear interpolation + + # We want to know whether to do signed (G75) or unsigned (G74) I/J offsets. These + # modes are independent of G01/G02/G03, e.g., Protel will issue multiple G03/G01 + # codes all in G75 mode. + # G74 -- Single-quadrant circular interpolation (disables multi-quadrant interpolation) + # G02/G03 codes set clockwise/counterclockwise arcs in a single quadrant only + # using X/Y/I/J commands with UNSIGNED (I,J). + # G75 -- Multi-quadrant circular interpolation --> X/Y/I/J with signed (I,J) + # G02/G03 codes set clockwise/counterclockwise arcs in all 4 quadrants + # using X/Y/I/J commands with SIGNED (I,J). + circ_signed = True # Assume G75...make sure this matches canned header we write out + + # If the very first flash/draw is a shorthand command (i.e., without an Xxxxx or Yxxxx) + # component then we don't really "see" the first point X00000Y00000. To account for this + # we use the following Boolean flag as well as the isLastShorthand flag during parsing + # to manually insert the point X000000Y00000 into the command stream. + firstFlash = True + + for line in fid: + # Get rid of CR characters (0x0D) and leading/trailing blanks + line = string.replace(line, '\x0D', '').strip() + + # Old location of format_pat search. Now moved down into the sub-line parse loop below. + + # RS-274X statement? If so, echo it. Currently, only the "LP" statement is expected + # (from Protel, of course). These will be distinguished from D-code and G-code + # commands by the fact that the first character of the string is '%'. + match = layerpol_pat.match(line) + if match: + self.commands[layername].append(line) + continue + + # See if this is an aperture definition, and if so, map it. + match = apdef_pat.match(line) + if match: + if currtool: + raise RuntimeError, "File %s has an aperture definition that comes after drawing commands." % fullname + + A = aptable.parseAperture(line, self.apmxlat[layername]) + if not A: + raise RuntimeError, "Unknown aperture definition in file %s" % fullname + + hash = A.hash() + if not RevGAT.has_key(hash): + #print line + #print self.apmxlat + #print RevGAT + raise RuntimeError, 'File %s has aperture definition "%s" not in global aperture table.' % (fullname, hash) + + # This says that all draw commands with this aperture code will + # be replaced by aperture self.apxlat[layername][code]. + self.apxlat[layername][A.code] = RevGAT[hash] + continue + + # Ignore %AMOC8* from Eagle for now as it uses a macro parameter, which + # is not yet supported in GerbMerge. + if line[:7]=='%AMOC8*': + continue + + # See if this is an aperture macro definition, and if so, map it. + M = amacro.parseApertureMacro(line,fid) + if M: + if currtool: + raise RuntimeError, "File %s has an aperture macro definition that comes after drawing commands." % fullname + + hash = M.hash() + if not RevGAMT.has_key(hash): + raise RuntimeError, 'File %s has aperture macro definition not in global aperture macro table:\n%s' % (fullname, hash) + + # This says that all aperture definition commands that reference this macro name + # will be replaced by aperture macro name self.apmxlat[layername][macroname]. + self.apmxlat[layername][M.name] = RevGAMT[hash] + continue + + # From this point on we may have more than one match on this line, e.g.: + # G54D11*X22400Y22300D02*X22500Y22200D01* + sub_line = line + while sub_line: + # Handle "comment" G-codes first + match = comment_pat.match(sub_line) + if match: + sub_line = sub_line[match.end():] + continue + + # See if this is a format statement, and if so, map it. In version 1.3 this was moved down + # from the line-only parse checks above (see comment) to handle OrCAD lines like + # G74*%FSLAN2X34Y34*% + match = format_pat.match(sub_line) # Used to be format_pat.search + if match: + sub_line = sub_line[match.end():] + for item in match.groups(): + if item is None: continue # Optional group didn't match + + if item[0] in "LA": # omit leading zeroes and absolute co-ordinates + continue + + if item[0]=='T': # omit trailing zeroes + raise RuntimeError, "Trailing zeroes not supported in RS274X files" + if item[0]=='I': # incremental co-ordinates + raise RuntimeError, "Incremental co-ordinates not supported in RS274X files" + + if item[0]=='N': # Maximum digits for N* commands...ignore it + continue + + if item[0]=='X': # M.N specification for X-axis. + fracpart = int(item[2]) + x_div = 10.0**(5-fracpart) + if item[0]=='Y': # M.N specification for Y-axis. + fracpart = int(item[2]) + y_div = 10.0**(5-fracpart) + continue + + # Parse and interpret G-codes + match = gcode_pat.match(sub_line) + if match: + sub_line = sub_line[match.end():] + gcode = int(match.group(1)) + + # Determine if this is a G-Code that should be ignored because it has no effect + # (e.g., G70 specifies "inches" which is already in effect). + if gcode in [54, 70, 90]: + continue + + # Determine if this is a G-Code that we have to emit because it matters. + if gcode in [1, 2, 3, 36, 37, 74, 75]: + self.commands[layername].append("G%02d" % gcode) + + # Determine if this is a G-code that sets a new mode + if gcode in [1, 36, 37]: + last_gmode = gcode + + # Remember last G74/G75 code so we know whether to do signed or unsigned I/J + # offsets. + if gcode==74: + circ_signed = False + elif gcode==75: + circ_signed = True + + continue + + raise RuntimeError, "G-Code 'G%02d' is not supported" % gcode + + # See if this is a tool change (aperture change) command + match = tool_pat.match(sub_line) + if match: + currtool = match.group(1) + + # Protel likes to issue random D01, D02, and D03 commands instead of aperture + # codes. We can ignore D01 because it simply means to move to the current location + # while drawing. Well, that's drawing a point. We can ignore D02 because it means + # to move to the current location without drawing. Truly pointless. We do NOT want + # to ignore D03 because it implies a flash. Protel very inefficiently issues a D02 + # move to a location without drawing, then a single-line D03 to flash. However, a D02 + # terminates a polygon in G36 mode, so keep D02's in this case. + if currtool=='D01' or (currtool=='D02' and (last_gmode != 36)): + sub_line = sub_line[match.end():] + continue + + if (currtool == 'D03') or (currtool=='D02' and (last_gmode == 36)): + self.commands[layername].append(currtool) + sub_line = sub_line[match.end():] + continue + + # Map it using our translation table + if not self.apxlat[layername].has_key(currtool): + raise RuntimeError, 'File %s has tool change command "%s" with no corresponding translation' % (fullname, currtool) + + currtool = self.apxlat[layername][currtool] + + # Add it to the list of things to write out + self.commands[layername].append(currtool) + + # Add it to the list of all apertures needed by this layer + self.apertures[layername].append(currtool) + + # Move on to next match, if any + sub_line = sub_line[match.end():] + continue + + # Is it a simple draw command? + I = J = None # For circular interpolation drawing commands + match = drawXY_pat.match(sub_line) + isLastShorthand = False # By default assume we don't make use of last_x and last_y + if match: + x, y, d = map(__builtin__.int, match.groups()) + else: + match = drawX_pat.match(sub_line) + if match: + x, d = map(__builtin__.int, match.groups()) + y = last_y + isLastShorthand = True # Indicate we're making use of last_x/last_y + else: + match = drawY_pat.match(sub_line) + if match: + y, d = map(__builtin__.int, match.groups()) + x = last_x + isLastShorthand = True # Indicate we're making use of last_x/last_y + + # Maybe it's a circular interpolation draw command with IJ components + if match is None: + match = cdrawXY_pat.match(sub_line) + if match: + x, y, I, J, d = map(__builtin__.int, match.groups()) + else: + match = cdrawX_pat.match(sub_line) + if match: + x, I, J, d = map(__builtin__.int, match.groups()) + y = last_y + isLastShorthand = True # Indicate we're making use of last_x/last_y + else: + match = cdrawY_pat.match(sub_line) + if match: + y, I, J, d = map(__builtin__.int, match.groups()) + x = last_x + isLastShorthand = True # Indicate we're making use of last_x/last_y + + if match: + if currtool is None: + # It's OK if this is an exposure-off movement command (specified with D02). + # It's also OK if we're in the middle of a G36 polygon fill as we're only defining + # the polygon extents. + if (d != 2) and (last_gmode != 36): + raise RuntimeError, 'File %s has draw command %s with no aperture chosen' % (fullname, sub_line) + + # Save last_x/y BEFORE scaling to 2.5 format else subsequent single-ordinate + # flashes (e.g., Y with no X) will be scaled twice! + last_x = x + last_y = y + + # Corner case: if this is the first flash/draw and we are using shorthand (i.e., missing Xxxx + # or Yxxxxx) then prepend the point X0000Y0000 into the commands as it is actually the starting + # point of our layer. We prepend the command X0000Y0000D02, i.e., a move to (0,0) without drawing. + if (isLastShorthand and firstFlash): + self.commands[layername].append((0,0,2)) + if updateExtents: + self.minx = min(self.minx,0) + self.maxx = max(self.maxx,0) + self.miny = min(self.miny,0) + self.maxy = max(self.maxy,0) + + x = int(round(x*x_div)) + y = int(round(y*y_div)) + if I is not None: + I = int(round(I*x_div)) + J = int(round(J*y_div)) + self.commands[layername].append((x,y,I,J,d,circ_signed)) + else: + self.commands[layername].append((x,y,d)) + firstFlash = False + + # Update dimensions...this is complicated for circular interpolation commands + # that span more than one quadrant. For now, we ignore this problem since users + # should be using a border layer to indicate extents. + if updateExtents: + if x < self.minx: self.minx = x + if x > self.maxx: self.maxx = x + if y < self.miny: self.miny = y + if y > self.maxy: self.maxy = y + + # Move on to next match, if any + sub_line = sub_line[match.end():] + continue + + # If it's none of the above, it had better be on our ignore list. + for pat in IgnoreList: + match = pat.match(sub_line) + if match: + break + else: + raise RuntimeError, 'File %s has uninterpretable line:\n %s' % (fullname, line) + + sub_line = sub_line[match.end():] + # end while still things to match on this line + # end of for each line in file + + fid.close() + if 0: + print layername + print self.commands[layername] + + def parseExcellon(self, fullname): + #print 'Reading data from %s ...' % fullname + + fid = file(fullname, 'rt') + currtool = None + suppress_leading = True # Suppress leading zeros by default, equivalent to 'INCH,TZ' + + # We store Excellon X/Y data in ten-thousandths of an inch. If the Config + # option ExcellonDecimals is not 4, we must adjust the values read from the + # file by a divisor to convert to ten-thousandths. This is only used in + # leading-zero suppression mode. In trailing-zero suppression mode, we must + # trailing-zero-pad all input integers to M+N digits (e.g., 6 digits for 2.4 mode) + # specified by the 'zeropadto' variable. + if self.ExcellonDecimals > 0: + divisor = 10.0**(4 - self.ExcellonDecimals) + zeropadto = 2+self.ExcellonDecimals + else: + divisor = 10.0**(4 - config.Config['excellondecimals']) + zeropadto = 2+config.Config['excellondecimals'] + + # Protel takes advantage of optional X/Y components when the previous one is the same, + # so we have to remember them. + last_x = last_y = 0 + + # Helper function to convert X/Y strings into integers in units of ten-thousandth of an inch. + def xln2tenthou(L, divisor=divisor, zeropadto=zeropadto): + V = [] + for s in L: + if not suppress_leading: + s = s + '0'*(zeropadto-len(s)) + V.append(int(round(int(s)*divisor))) + return tuple(V) + + for line in fid.xreadlines(): + # Get rid of CR characters + line = string.replace(line, '\x0D', '') + + # Protel likes to embed comment lines beginning with ';' + if line[0]==';': + continue + + # Check for leading/trailing zeros included ("INCH,LZ" or "INCH,TZ") + match = xzsup_pat.match(line) + if match: + if match.group(1)=='L': + # LZ --> Leading zeros INCLUDED + suppress_leading = False + else: + # TZ --> Trailing zeros INCLUDED + suppress_leading = True + continue + + # See if a tool is being defined. First try to match with tool name+size + match = xtdef_pat.match(line) # xtdef_pat and xtdef2_pat expect tool name and diameter + if match is None: # but xtdef_pat expects optional feed/speed between T and C + match = xtdef2_pat.match(line) # and xtdef_2pat expects feed/speed at the end + if match: + currtool, diam = match.groups() + try: + diam = float(diam) + except: + raise RuntimeError, "File %s has illegal tool diameter '%s'" % (fullname, diam) + + # Canonicalize tool number because Protel (of course) sometimes specifies it + # as T01 and sometimes as T1. We canonicalize to T01. + currtool = 'T%02d' % int(currtool[1:]) + + if self.xdiam.has_key(currtool): + raise RuntimeError, "File %s defines tool %s more than once" % (fullname, currtool) + self.xdiam[currtool] = diam + continue + + # Didn't match TxxxCyyy. It could be a tool change command 'Tdd'. + match = xtool_pat.match(line) + if match: + currtool = match.group(1) + + # Canonicalize tool number because Protel (of course) sometimes specifies it + # as T01 and sometimes as T1. We canonicalize to T01. + currtool = 'T%02d' % int(currtool[1:]) + + # Diameter will be obtained from embedded tool definition, local tool list or if not found, the global tool list + try: + diam = self.xdiam[currtool] + except: + if self.ToolList: + try: + diam = self.ToolList[currtool] + except: + raise RuntimeError, "File %s uses tool code %s that is not defined in the job's tool list" % (fullname, currtool) + else: + try: + diam = config.DefaultToolList[currtool] + except: + #print config.DefaultToolList + raise RuntimeError, "File %s uses tool code %s that is not defined in default tool list" % (fullname, currtool) + + self.xdiam[currtool] = diam + continue + + # Plunge command? + match = xydraw_pat.match(line) + if match: + x, y = xln2tenthou(match.groups()) + else: + match = xdraw_pat.match(line) + if match: + x = xln2tenthou(match.groups())[0] + y = last_y + else: + match = ydraw_pat.match(line) + if match: + y = xln2tenthou(match.groups())[0] + x = last_x + + if match: + if currtool is None: + raise RuntimeError, 'File %s has plunge command without previous tool selection' % fullname + + try: + self.xcommands[currtool].append((x,y)) + except KeyError: + self.xcommands[currtool] = [(x,y)] + + last_x = x + last_y = y + continue + + # It had better be an ignorable + for pat in XIgnoreList: + if pat.match(line): + break + else: + raise RuntimeError, 'File %s has uninterpretable line:\n %s' % (fullname, line) + + def hasLayer(self, layername): + return self.commands.has_key(layername) + + def writeGerber(self, fid, layername, Xoff, Yoff): + "Write out the data such that the lower-left corner of this job is at the given (X,Y) position, in inches" + + # Maybe we don't have this layer + if not self.hasLayer(layername): return + + # First convert given inches to 2.5 co-ordinates + X = int(round(Xoff/0.00001)) + Y = int(round(Yoff/0.00001)) + + # Now calculate displacement for each position so that we end up at specified origin + DX = X - self.minx + DY = Y - self.miny + + # Rock and roll. First, write out a dummy flash using code D02 + # (exposure off). This prevents an unintentional draw from the end + # of one job to the beginning of the next when a layer is repeated + # due to panelizing. + fid.write('X%07dY%07dD02*\n' % (X, Y)) + for cmd in self.commands[layername]: + if type(cmd) is types.TupleType: + if len(cmd)==3: + x, y, d = cmd + fid.write('X%07dY%07dD%02d*\n' % (x+DX, y+DY, d)) + else: + x, y, I, J, d, s = cmd + fid.write('X%07dY%07dI%07dJ%07dD%02d*\n' % (x+DX, y+DY, I, J, d)) # I,J are relative + else: + # It's an aperture change, G-code, or RS274-X command that begins with '%'. If + # it's an aperture code, the aperture has already been translated + # to the global aperture table during the parse phase. + if cmd[0]=='%': + fid.write('%s\n' % cmd) # The command already has a * in it (e.g., "%LPD*%") + else: + fid.write('%s*\n' % cmd) + + def findTools(self, diameter): + "Find the tools, if any, with the given diameter in inches. There may be more than one!" + L = [] + for tool, diam in self.xdiam.items(): + if diam==diameter: + L.append(tool) + return L + + def writeExcellon(self, fid, diameter, Xoff, Yoff): + "Write out the data such that the lower-left corner of this job is at the given (X,Y) position, in inches" + + # First convert given inches to 2.4 co-ordinates. Note that Gerber is 2.5 (as of GerbMerge 1.2) + # and our internal Excellon representation is 2.4 as of GerbMerge + # version 0.91. We use X,Y to calculate DX,DY in 2.4 units (i.e., with a + # resolution of 0.0001". + X = int(round(Xoff/0.00001)) # First work in 2.5 format to match Gerber + Y = int(round(Yoff/0.00001)) + + # Now calculate displacement for each position so that we end up at specified origin + DX = X - self.minx + DY = Y - self.miny + + # Now round down to 2.4 format + DX = int(round(DX/10.0)) + DY = int(round(DY/10.0)) + + ltools = self.findTools(diameter) + + if config.Config['excellonleadingzeros']: + fmtstr = 'X%06dY%06d\n' + else: + fmtstr = 'X%dY%d\n' + + # Boogie + for ltool in ltools: + if self.xcommands.has_key(ltool): + for cmd in self.xcommands[ltool]: + x, y = cmd + fid.write(fmtstr % (x+DX, y+DY)) + + def writeDrillHits(self, fid, diameter, toolNum, Xoff, Yoff): + """Write a drill hit pattern. diameter is tool diameter in inches, while toolNum is + an integer index into strokes.DrillStrokeList""" + + # First convert given inches to 2.5 co-ordinates + X = int(round(Xoff/0.00001)) + Y = int(round(Yoff/0.00001)) + + # Now calculate displacement for each position so that we end up at specified origin + DX = X - self.minx + DY = Y - self.miny + + # Do NOT round down to 2.4 format. These drill hits are in Gerber 2.5 format, not + # Excellon plunge commands. + + ltools = self.findTools(diameter) + + for ltool in ltools: + if self.xcommands.has_key(ltool): + for cmd in self.xcommands[ltool]: + x, y = cmd + makestroke.drawDrillHit(fid, 10*x+DX, 10*y+DY, toolNum) + + def aperturesAndMacros(self, layername): + "Return dictionaries whose keys are all necessary aperture names and macro names for this layer" + + GAT=config.GAT + + if self.apertures.has_key(layername): + apdict = {}.fromkeys(self.apertures[layername]) + apmlist = [GAT[ap].dimx for ap in self.apertures[layername] if GAT[ap].apname=='Macro'] + apmdict = {}.fromkeys(apmlist) + + return apdict, apmdict + else: + return {}, {} + + def makeLocalApertureCode(self, layername, AP): + "Find or create a layer-specific aperture code to represent the global aperture given" + if AP.code not in self.apxlat[layername].values(): + lastCode = aptable.findHighestApertureCode(self.apxlat[layername].keys()) + localCode = 'D%d' % (lastCode+1) + self.apxlat[layername][localCode] = AP.code + + def inBorders(self, x, y): + return (x >= self.minx) and (x <= self.maxx) and (y >= self.miny) and (y <= self.maxy) + + def trimGerberLayer(self, layername): + "Modify drawing commands that are outside job dimensions" + + newcmds = [] + lastInBorders = True + lastx, lasty, lastd = self.minx, self.miny, 2 # (minx,miny,exposure off) + bordersRect = (self.minx, self.miny, self.maxx, self.maxy) + lastAperture = None + + for cmd in self.commands[layername]: + if type(cmd) == types.TupleType: + # It is a data command: tuple (X, Y, D), all integers, or (X, Y, I, J, D), all integers. + if len(cmd)==3: + x, y, d = cmd + # I=J=None # In case we support circular interpolation in the future + else: + # We don't do anything with circular interpolation for now, so just issue + # the command and be done with it. + # x, y, I, J, d, s = cmd + newcmds.append(cmd) + continue + + newInBorders = self.inBorders(x,y) + + # Flash commands are easy (for now). If they're outside borders, + # ignore them. There's no need to consider the previous command. + # What should we do if the flash is partially inside and partially + # outside the border? Ideally, define a macro that constructs the + # part of the flash that is inside the border. Practically, you've + # got to be kidding. + # + # Actually, it's not that tough for rectangle apertures. We identify + # the intersection rectangle of the aperture and the bounding box, + # determine the new rectangular aperture required along with the + # new flash point, add the aperture to the GAT if necessary, and + # make the change. Spiffy. + # + # For circular interpolation commands, it's definitely harder since + # we have to construct arcs that are a subset of the original arc. + # + # For polygon fills, we similarly have to break up the polygon into + # sub-polygons that are contained within the allowable extents. + # + # Both circular interpolation and polygon fills are a) uncommon, + # and b) hard to handle. The current version of GerbMerge does not + # handle these cases. + if d==3: + if lastAperture.isRectangle(): + apertureRect = lastAperture.rectangleAsRect(x,y) + if geometry.isRect1InRect2(apertureRect, bordersRect): + newcmds.append(cmd) + else: + newRect = geometry.intersectExtents(apertureRect, bordersRect) + + if newRect: + newRectWidth = geometry.rectWidth(newRect) + newRectHeight = geometry.rectHeight(newRect) + newX, newY = geometry.rectCenter(newRect) + + # We arbitrarily remove all flashes that lead to rectangles + # with a width or length less than 1 mil (10 Gerber units). + # Should we make this configurable? + if min(newRectWidth, newRectHeight) >= 10: + # Construct an Aperture that is a Rectangle of dimensions (newRectWidth,newRectHeight) + newAP = aptable.Aperture(aptable.Rectangle, 'D??', \ + util.gerb2in(newRectWidth), util.gerb2in(newRectHeight)) + global_code = aptable.findOrAddAperture(newAP) + + # We need an unused local aperture code to correspond to this newly-created global one. + self.makeLocalApertureCode(layername, newAP) + + # Make sure to indicate that the new aperture is one that is used by this layer + if global_code not in self.apertures[layername]: + self.apertures[layername].append(global_code) + + # Switch to new aperture code, flash new aperture, switch back to previous aperture code + newcmds.append(global_code) + newcmds.append((newX, newY, 3)) + newcmds.append(lastAperture.code) + else: + pass # Ignore this flash...area in common is too thin + else: + pass # Ignore this flash...no area in common + elif self.inBorders(x, y): + # Aperture is not a rectangle and its center is somewhere within our + # borders. Flash it and ignore part outside borders (for now). + newcmds.append(cmd) + else: + pass # Ignore this flash + + # If this is a exposure off command, then it doesn't matter what the + # previous command is. This command just updates the (X,Y) position + # and sets the start point for a line draw to a new location. + elif d==2: + if self.inBorders(x, y): + newcmds.append(cmd) + + else: + # This is an exposure on (draw line) command. Now things get interesting. + # Regardless of what the last command was (draw, exposure off, flash), we + # are planning on drawing a visible line using the current aperture from + # the (lastx,lasty) position to the new (x,y) position. The cases are: + # A: (lastx,lasty) is outside borders, (x,y) is outside borders. + # (lastx,lasty) have already been eliminated. Just update (lastx,lasty) + # with new (x,y) and remove the new command too. There is one case which + # may be of concern, and that is when the line defined by (lastx,lasty)-(x,y) + # actually crosses through the job. In this case, we have to draw the + # partial line (x1,y1)-(x2,y2) where (x1,y1) and (x2,y2) lie on the + # borders. We will add 3 commands: + # X(x1)Y(y1)D02 # exposure off + # X(x2)Y(y2)D01 # exposure on + # X(x)Y(y)D02 # exposure off + # + # B: (lastx,lasty) is outside borders, (x,y) is inside borders. + # We have to find the intersection of the line (lastx,lasty)-(x,y) + # with the borders and draw only the line segment (x1,y1)-(x,y): + # X(x1)Y(y1)D02 # exposure off + # X(x)Y(y)D01 # exposure on + # + # C: (lastx,lasty) is inside borders, (x,y) is outside borders. + # We have to find the intersection of the line (lastx,lasty)-(x,y) + # with the borders and draw only the line segment (lastx,lasty)-(x1,y1): + # then update to the new position: + # X(x1)Y(y1)D01 # exposure on + # X(x)Y(y)D02 # exposure off + # + # D: (lastx,lasty) is inside borders, (x,y) is inside borders. This is + # the most common and simplest case...just copy the command over: + # X(x)Y(y)D01 # exposure on + # + # All of the above are for linear interpolation. Circular interpolation + # is ignored for now. + if lastInBorders and newInBorders: # Case D + newcmds.append(cmd) + + else: + # segmentXbox() returns a list of 0, 1, or 2 points describing the intersection + # points of the segment (lastx,lasty)-(x,y) with the box defined + # by lower-left corner (minx,miny) and upper-right corner (maxx,maxy). + pointsL = geometry.segmentXbox((lastx,lasty), (x,y), (self.minx,self.miny), (self.maxx,self.maxy)) + + if len(pointsL)==0: # Case A, no intersection + # Both points are outside the box and there is no overlap with box. + d = 2 # Command is effectively removed since newcmds wasn't extended. + # Ensure "last command" is exposure off to reflect this. + + elif len(pointsL)==1: # Cases B and C + pt1 = pointsL[0] + if newInBorders: # Case B + newcmds.append((pt1[0], pt1[1], 2)) # Go to intersection point, exposure off + newcmds.append(cmd) # Go to destination point, exposure on + else: # Case C + newcmds.append((pt1[0], pt1[1], 1)) # Go to intersection point, exposure on + newcmds.append((x, y, 2)) # Go to destination point, exposure off + d = 2 # Make next 'lastd' represent exposure off + + else: # Case A, two points of intersection + pt1 = pointsL[0] + pt2 = pointsL[1] + + newcmds.append((pt1[0], pt1[1], 2)) # Go to first intersection point, exposure off + newcmds.append((pt2[0], pt2[1], 1)) # Draw to second intersection point, exposure on + newcmds.append((x, y, 2)) # Go to destination point, exposure off + d = 2 # Make next 'lastd' represent exposure off + + lastx, lasty, lastd = x, y, d + lastInBorders = newInBorders + else: + # It's a string indicating an aperture change, G-code, or RS-274X + # command (e.g., "D13", "G75", "%LPD*%") + newcmds.append(cmd) + if cmd[0]=='D' and int(cmd[1:])>=10: # Don't interpret D01, D02, D03 + lastAperture = config.GAT[cmd] + + self.commands[layername] = newcmds + + def trimGerber(self): + for layername in self.commands.keys(): + self.trimGerberLayer(layername) + + def trimExcellon(self): + "Remove plunge commands that are outside job dimensions" + keys = self.xcommands.keys() + for toolname in keys: + # Remember Excellon is 2.4 format while Gerber data is 2.5 format + validList = [(x,y) for x,y in self.xcommands[toolname] if self.inBorders(10*x,10*y)] + + if validList: + self.xcommands[toolname] = validList + else: + del self.xcommands[toolname] + del self.xdiam[toolname] + +# This class encapsulates a Job object, providing absolute +# positioning information. +class JobLayout: + def __init__(self, job): + self.job = job + self.x = None + self.y = None + + def canonicalize(self): # Must return a JobLayout object as a list + return [self] + + def writeGerber(self, fid, layername): + assert self.x is not None + self.job.writeGerber(fid, layername, self.x, self.y) + + def aperturesAndMacros(self, layername): + return self.job.aperturesAndMacros(layername) + + def writeExcellon(self, fid, diameter): + assert self.x is not None + self.job.writeExcellon(fid, diameter, self.x, self.y) + + def writeDrillHits(self, fid, diameter, toolNum): + assert self.x is not None + self.job.writeDrillHits(fid, diameter, toolNum, self.x, self.y) + + def writeCutLines(self, fid, drawing_code, X1, Y1, X2, Y2): + """Draw a board outline using the given aperture code""" + def notEdge(x, X): + return round(abs(1000*(x-X))) + + assert self.x and self.y + + radius = config.GAT[drawing_code].dimx/2.0 + + # Start at lower-left, proceed clockwise + x = self.x - radius + y = self.y - radius + + left = notEdge(self.x, X1) + right = notEdge(self.x+self.width_in(), X2) + bot = notEdge(self.y, Y1) + top = notEdge(self.y+self.height_in(), Y2) + + BL = ((x), (y)) + TL = ((x), (y+self.height_in()+2*radius)) + TR = ((x+self.width_in()+2*radius), (y+self.height_in()+2*radius)) + BR = ((x+self.width_in()+2*radius), (y)) + + if not left: + BL = (BL[0]+2*radius, BL[1]) + TL = (TL[0]+2*radius, TL[1]) + + if not top: + TL = (TL[0], TL[1]-2*radius) + TR = (TR[0], TR[1]-2*radius) + + if not right: + TR = (TR[0]-2*radius, TR[1]) + BR = (BR[0]-2*radius, BR[1]) + + if not bot: + BL = (BL[0], BL[1]+2*radius) + BR = (BR[0], BR[1]+2*radius) + + BL = (util.in2gerb(BL[0]), util.in2gerb(BL[1])) + TL = (util.in2gerb(TL[0]), util.in2gerb(TL[1])) + TR = (util.in2gerb(TR[0]), util.in2gerb(TR[1])) + BR = (util.in2gerb(BR[0]), util.in2gerb(BR[1])) + + # The "if 1 or ..." construct draws all four sides of the job. By + # removing the 1 from the expression, only the sides that do not + # correspond to panel edges are drawn. The former is probably better + # since panels tend to have a little slop from the cutting operation + # and it's easier to just cut it smaller when there's a cut line. + # The way it is now with "if 1 or....", much of this function is + # unnecessary. Heck, we could even just use the boardoutline layer + # directly. + if 1 or left: + fid.write('X%07dY%07dD02*\n' % BL) + fid.write('X%07dY%07dD01*\n' % TL) + + if 1 or top: + if not left: fid.write('X%07dY%07dD02*\n' % TL) + fid.write('X%07dY%07dD01*\n' % TR) + + if 1 or right: + if not top: fid.write('X%07dY%07dD02*\n' % TR) + fid.write('X%07dY%07dD01*\n' % BR) + + if 1 or bot: + if not right: fid.write('X%07dY%07dD02*\n' % BR) + fid.write('X%07dY%07dD01*\n' % BL) + + def setPosition(self, x, y): + self.x=x + self.y=y + + def width_in(self): + return self.job.width_in() + + def height_in(self): + return self.job.height_in() + + def drillhits(self, diameter): + tools = self.job.findTools(diameter) + total = 0 + for tool in tools: + try: + total += len(self.job.xcommands[tool]) + except: + pass + + return total + + def jobarea(self): + return self.job.jobarea() + +def rotateJob(job, degrees = 90, firstpass = True): + """Create a new job from an existing one, rotating by specified degrees in 90 degree passes""" + GAT = config.GAT + GAMT = config.GAMT + ##print "rotating job:", job.name, degrees, firstpass + if firstpass: + if degrees == 270: + J = Job(job.name+'*rotated270') + elif degrees == 180: + J = Job(job.name+'*rotated180') + else: + J = Job(job.name+'*rotated90') + else: + J = Job(job.name) + + # Keep the origin (lower-left) in the same place + J.maxx = job.minx + job.maxy-job.miny + J.maxy = job.miny + job.maxx-job.minx + J.minx = job.minx + J.miny = job.miny + + RevGAT = config.buildRevDict(GAT) # RevGAT[hash] = aperturename + RevGAMT = config.buildRevDict(GAMT) # RevGAMT[hash] = aperturemacroname + + # Keep list of tool diameters and default tool list + J.xdiam = job.xdiam + J.ToolList = job.ToolList + J.Repeat = job.Repeat + + # D-code translation table is the same, except we have to rotate + # those apertures which have an orientation: rectangles, ovals, and macros. + + ToolChangeReplace = {} + for layername in job.apxlat.keys(): + J.apxlat[layername] = {} + + for ap in job.apxlat[layername].keys(): + code = job.apxlat[layername][ap] + A = GAT[code] + + if A.apname in ('Circle', 'Octagon'): + # This aperture is fine. Copy it over. + J.apxlat[layername][ap] = code + continue + + # Must rotate the aperture + APR = A.rotated(RevGAMT) + + # Does it already exist in the GAT? + hash = APR.hash() + try: + # Yup...add it to apxlat + newcode = RevGAT[hash] + except KeyError: + # Must add new aperture to GAT + newcode = aptable.addToApertureTable(APR) + + # Rebuild RevGAT + #RevGAT = config.buildRevDict(GAT) + RevGAT[hash] = newcode + + J.apxlat[layername][ap] = newcode + + # Must also replace all tool change commands from + # old code to new command. + ToolChangeReplace[code] = newcode + + # Now we copy commands, rotating X,Y positions. + # Rotations will occur counterclockwise about the + # point (minx,miny). Then, we shift to the right + # by the height so that the lower-left point of + # the rotated job continues to be (minx,miny). + # + # We also have to take aperture change commands and + # replace them with the new aperture code if we have + # a rotation. + offset = job.maxy-job.miny + for layername in job.commands.keys(): + J.commands[layername] = [] + J.apertures[layername] = [] + + for cmd in job.commands[layername]: + # Is it a drawing command? + if type(cmd) is types.TupleType: + if len(cmd)==3: + x, y, d = map(__builtin__.int, cmd) + II=JJ=None + else: + x, y, II, JJ, d, signed = map(__builtin__.int, cmd) # J is already used as Job object + else: + # No, must be a string indicating aperture change, G-code, or RS274-X command. + if cmd[0] in ('G', '%'): + # G-codes and RS274-X commands are just copied verbatim and not affected by rotation + J.commands[layername].append(cmd) + continue + + # It's a D-code. See if we need to replace aperture changes with a rotated aperture. + # But only for D-codes >= 10. + if int(cmd[1:]) < 10: + J.commands[layername].append(cmd) + continue + + try: + newcmd = ToolChangeReplace[cmd] + J.commands[layername].append(newcmd) + J.apertures[layername].append(newcmd) + except KeyError: + J.commands[layername].append(cmd) + J.apertures[layername].append(cmd) + continue + + # (X,Y) --> (-Y,X) effects a 90-degree counterclockwise shift + # Adding 'offset' to -Y maintains the lower-left origin of (minx,miny). + newx = -(y - job.miny) + job.minx + offset + newy = (x-job.minx) + job.miny + + # For circular interpolation commands, (I,J) components are always relative + # so we do not worry about offsets, just reverse their sense, i.e., I becomes J + # and J becomes I. For 360-degree circular interpolation, I/J are signed and we + # must map (I,J) --> (-J,I). + if II is not None: + if signed: + J.commands[layername].append((newx, newy, -JJ, II, d, signed)) + else: + J.commands[layername].append((newx, newy, JJ, II, d, signed)) + else: + J.commands[layername].append((newx,newy,d)) + + if 0: + print job.minx, job.miny, offset + print layername + print J.commands[layername] + + # Finally, rotate drills. Offset is in hundred-thousandths (2.5) while Excellon + # data is in 2.4 format. + for tool in job.xcommands.keys(): + J.xcommands[tool] = [] + + for x,y in job.xcommands[tool]: + newx = -(10*y - job.miny) + job.minx + offset + newy = (10*x - job.minx) + job.miny + + newx = int(round(newx/10.0)) + newy = int(round(newy/10.0)) + + J.xcommands[tool].append((newx,newy)) + + # Rotate some more if required + degrees -= 90 + if degrees > 0: + return rotateJob(J, degrees, False) + else: + ##print "rotated:", J.name + return J diff --git a/gerbmerge/makestroke.py b/gerbmerge/makestroke.py new file mode 100644 index 0000000..551ef7f --- /dev/null +++ b/gerbmerge/makestroke.py @@ -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() diff --git a/gerbmerge/parselayout.py b/gerbmerge/parselayout.py new file mode 100644 index 0000000..3e17326 --- /dev/null +++ b/gerbmerge/parselayout.py @@ -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)) diff --git a/gerbmerge/placement.py b/gerbmerge/placement.py new file mode 100644 index 0000000..598fa58 --- /dev/null +++ b/gerbmerge/placement.py @@ -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) + diff --git a/gerbmerge/schwartz.py b/gerbmerge/schwartz.py new file mode 100644 index 0000000..e243961 --- /dev/null +++ b/gerbmerge/schwartz.py @@ -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) diff --git a/gerbmerge/scoring.py b/gerbmerge/scoring.py new file mode 100644 index 0000000..ecb2b80 --- /dev/null +++ b/gerbmerge/scoring.py @@ -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 diff --git a/gerbmerge/specs.py b/gerbmerge/specs.py new file mode 100644 index 0000000..5c76b13 --- /dev/null +++ b/gerbmerge/specs.py @@ -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 * +* - ViewMate * +* * +* Linux: * +* - gerbv * +* * +* 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=" + + diff --git a/gerbmerge/strokes.py b/gerbmerge/strokes.py new file mode 100644 index 0000000..565e74d --- /dev/null +++ b/gerbmerge/strokes.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# This file was automatically generated by genstroke.py + +StrokeMap = {'"': [[(400, 500), (300, 400)], [(400, 500), (400, 700)], [(200, 700), (200, 500), (100, 400)]], '$': [[(250, -100), (250, 700)], [(350, 600), (450, 500)], [(350, 600), (150, 600), (50, 500), (50, 400), (150, 300), (350, 300), (450, 200), (450, 100), (350, 0), (150, 0), (50, 100)]], '&': [[(150, 600), (50, 500), (50, 400), (450, 0)], [(450, 200), (250, 0), (150, 0), (50, 100), (50, 200), (250, 400), (250, 500), (150, 600)]], '(': [[(350, 600), (150, 400), (150, 200), (350, 0)]], '*': [[(450, 300), (50, 300)], [(50, 500), (450, 100)], [(250, 100), (250, 500)], [(450, 500), (50, 100)]], ',': [[(150, -200), (350, 0), (250, 0), (250, 100), (350, 100), (350, 0)]], '.': [[(200, 100), (300, 100), (300, 0), (200, 0), (200, 100)]], '0': [[(150, 0), (50, 100), (450, 500), (450, 100), (350, 0), (150, 0)], [(50, 100), (50, 500), (150, 600), (350, 600), (450, 500)]], '2': [[(50, 0), (450, 400), (450, 500), (350, 600), (150, 600), (50, 500)], [(50, 0), (450, 0)]], '4': [[(50, 300), (450, 300)], [(350, 0), (350, 600), (50, 300)]], '6': [[(50, 300), (50, 100), (150, 0), (350, 0), (450, 100), (450, 200), (350, 300), (50, 300), (250, 500), (450, 600)]], '8': [[(50, 100), (50, 200), (150, 300), (350, 300), (450, 200), (450, 100), (350, 0), (150, 0), (50, 100)], [(150, 300), (50, 400), (50, 500), (150, 600), (350, 600), (450, 500), (450, 400), (350, 300)]], ':': [[(200, 400), (200, 300), (300, 300), (300, 400), (200, 400)], [(200, 100), (200, 0), (300, 0), (300, 100), (200, 100)]], '<': [[(50, 300), (450, 0)], [(50, 300), (450, 600)]], '>': [[(450, 300), (50, 0)], [(450, 300), (50, 600)]], '@': [[(450, 100), (350, 0), (150, 0), (50, 100), (50, 500), (150, 600), (350, 600), (450, 500), (450, 300), (350, 200), (350, 400), (150, 400), (150, 200), (350, 200)]], 'B': [[(50, 300), (350, 300), (450, 200), (450, 100), (350, 0), (50, 0), (50, 600), (350, 600), (450, 500), (450, 400), (350, 300)]], 'D': [[(50, 600), (50, 0), (350, 0), (450, 100), (450, 500), (350, 600), (50, 600)]], 'F': [[(50, 300), (250, 300)], [(50, 600), (450, 600)], [(50, 600), (50, 0)]], 'H': [[(50, 0), (50, 600)], [(50, 300), (450, 300)], [(450, 600), (450, 0)]], 'J': [[(50, 100), (150, 0), (250, 0), (350, 100), (350, 600)], [(250, 600), (450, 600)]], 'L': [[(50, 0), (450, 0)], [(50, 0), (50, 600)]], 'N': [[(50, 0), (50, 600), (450, 0), (450, 600)]], 'P': [[(450, 300), (350, 200), (50, 200)], [(50, 0), (50, 600), (350, 600), (450, 500), (450, 300)]], 'R': [[(450, 300), (350, 200), (50, 200)], [(50, 0), (50, 600), (350, 600), (450, 500), (450, 300)], [(250, 200), (450, 0)]], 'T': [[(450, 600), (50, 600)], [(250, 600), (250, 0)]], 'V': [[(450, 600), (450, 200), (250, 0), (50, 200), (50, 600)]], 'X': [[(450, 0), (50, 600)], [(450, 600), (50, 0)]], 'Z': [[(50, 600), (450, 600), (450, 500), (50, 100), (50, 0), (450, 0)]], '\\': [[(450, 0), (50, 600)]], '^': [[(450, 300), (250, 500), (50, 300)]], '`': [[(200, 700), (200, 500), (300, 400)]], 'b': [[(450, 100), (450, 300), (350, 400), (50, 400)], [(50, 600), (50, 0), (350, 0), (450, 100)]], 'd': [[(450, 400), (150, 400), (50, 300), (50, 100), (150, 0), (450, 0), (450, 600)]], 'f': [[(350, 300), (150, 300)], [(250, 500), (250, 0)], [(250, 500), (350, 600)]], 'h': [[(450, 300), (450, 0)], [(450, 300), (350, 400), (150, 400), (50, 300)], [(50, 600), (50, 0)]], 'j': [[(250, -200), (350, -100), (350, 400)], [(350, 600), (350, 700)], [(250, -200), (150, -200)]], 'l': [[(350, 0), (150, 0)], [(250, 0), (250, 600), (150, 600)]], 'n': [[(450, 0), (450, 300), (350, 400), (50, 400), (50, 0)]], 'p': [[(50, -200), (50, 400), (350, 400), (450, 300), (450, 100), (350, 0), (50, 0)]], 'r': [[(400, 400), (300, 400), (100, 200)], [(100, 0), (100, 400)]], 't': [[(250, 500), (250, 100), (350, 0)], [(350, 400), (150, 400)]], 'v': [[(50, 400), (250, 0), (450, 400)]], 'x': [[(50, 400), (450, 0)], [(50, 0), (450, 400)]], 'z': [[(50, 400), (450, 400), (50, 0), (450, 0)]], '|': [[(250, 0), (250, 600)]], '~': [[(50, 600), (150, 700), (350, 500), (450, 600)]], '!': [[(250, 0), (250, 100)], [(250, 200), (250, 700)]], '#': [[(450, 200), (50, 200)], [(50, 400), (350, 400), (450, 400)], [(350, 600), (350, 0)], [(150, 0), (150, 600)]], '%': [[(500, 600), (0, 0)], [(0, 500), (0, 600), (100, 600), (100, 500), (0, 500)], [(400, 100), (500, 100), (500, 0), (400, 0), (400, 100)]], "'": [[(200, 400), (300, 500), (300, 700)]], ')': [[(150, 600), (350, 400), (350, 200), (150, 0)]], '+': [[(250, 100), (250, 500)], [(50, 300), (450, 300)]], '-': [[(50, 300), (450, 300)]], '/': [[(50, 0), (450, 600)]], '1': [[(150, 500), (250, 600), (250, 0)], [(150, 0), (350, 0)]], '3': [[(50, 100), (150, 0), (350, 0), (450, 100), (450, 200), (350, 300), (250, 300)], [(350, 300), (450, 400), (450, 500), (350, 600), (150, 600), (50, 500)]], '5': [[(50, 300), (250, 400), (350, 400), (450, 300), (450, 100), (350, 0), (150, 0), (50, 100)], [(50, 300), (50, 600), (450, 600)]], '7': [[(50, 600), (450, 600), (450, 500), (50, 100), (50, 0)]], '9': [[(50, 400), (150, 300), (450, 300)], [(450, 100), (450, 500), (350, 600), (150, 600), (50, 500), (50, 400)], [(50, 100), (150, 0), (350, 0), (450, 100)]], ';': [[(150, -200), (350, 0), (250, 0), (250, 100), (350, 100), (350, 0)], [(350, 300), (250, 300), (250, 400), (350, 400), (350, 300)]], '=': [[(450, 400), (50, 400)], [(50, 200), (450, 200)]], '?': [[(450, 400), (250, 200)], [(250, 100), (250, 0)], [(450, 400), (450, 500), (350, 600), (150, 600), (50, 500)]], 'A': [[(50, 0), (50, 400), (250, 600), (450, 400), (450, 0)], [(450, 300), (50, 300)]], 'C': [[(50, 500), (50, 100), (150, 0), (350, 0), (450, 100)], [(450, 500), (350, 600), (150, 600), (50, 500)]], 'E': [[(450, 600), (50, 600), (50, 0), (450, 0)], [(250, 300), (50, 300)]], 'G': [[(50, 100), (150, 0), (350, 0), (450, 100), (450, 300), (250, 300)], [(450, 500), (350, 600), (150, 600), (50, 500), (50, 100)]], 'I': [[(150, 0), (350, 0)], [(250, 0), (250, 600)], [(150, 600), (350, 600)]], 'K': [[(50, 600), (50, 0)], [(50, 200), (450, 600)], [(150, 300), (450, 0)]], 'M': [[(50, 0), (50, 600), (250, 400), (450, 600), (450, 0)]], 'O': [[(450, 100), (450, 500), (350, 600), (150, 600), (50, 500), (50, 100), (150, 0), (350, 0), (450, 100)]], 'Q': [[(450, 0), (250, 200)], [(350, 0), (150, 0), (50, 100), (50, 500), (150, 600), (350, 600), (450, 500), (450, 100), (350, 0)]], 'S': [[(450, 100), (350, 0), (150, 0), (50, 100)], [(150, 300), (50, 400), (50, 500), (150, 600), (350, 600), (450, 500)], [(350, 300), (450, 200), (450, 100)], [(350, 300), (150, 300)]], 'U': [[(450, 600), (450, 100), (350, 0), (150, 0), (50, 100), (50, 600)]], 'W': [[(450, 0), (450, 600)], [(250, 200), (450, 0)], [(250, 200), (50, 0), (50, 600)]], 'Y': [[(50, 600), (50, 500), (250, 300), (250, 0)], [(250, 300), (450, 500), (450, 600)]], '[': [[(350, 600), (150, 600), (150, 0), (350, 0)]], ']': [[(350, 0), (350, 600), (150, 600)], [(150, 0), (350, 0)]], '_': [[(450, -100), (50, -100)]], 'a': [[(450, 200), (150, 200), (50, 100), (150, 0), (450, 0), (450, 300), (350, 400), (150, 400)]], 'c': [[(450, 400), (150, 400), (50, 300), (50, 100), (150, 0), (450, 0)]], 'e': [[(450, 300), (450, 200), (50, 200)], [(50, 300), (150, 400), (350, 400), (450, 300)], [(350, 0), (150, 0), (50, 100), (50, 300)]], 'g': [[(450, 0), (150, 0), (50, 100), (50, 300), (150, 400), (450, 400), (450, -100), (350, -200), (250, -200)]], 'i': [[(350, 0), (150, 0)], [(250, 0), (250, 400), (150, 400)], [(250, 600), (250, 700)]], 'k': [[(400, 400), (100, 200), (400, 0)], [(100, 0), (100, 600)]], 'm': [[(450, 0), (450, 300), (350, 400), (250, 300), (250, 0)], [(250, 300), (150, 400), (50, 400), (50, 0)]], 'o': [[(450, 100), (450, 300), (350, 400), (150, 400), (50, 300), (50, 100), (150, 0), (350, 0), (450, 100)]], 'q': [[(450, 400), (450, -200)], [(450, 0), (150, 0), (50, 100), (50, 300), (150, 400), (450, 400)]], 's': [[(450, 400), (150, 400), (50, 300), (150, 200), (350, 200), (450, 100), (350, 0), (50, 0)]], 'u': [[(150, 0), (450, 0), (450, 400)], [(50, 400), (50, 100), (150, 0)]], 'w': [[(50, 400), (50, 100), (150, 0), (250, 100), (350, 0), (450, 100), (450, 400)]], 'y': [[(50, 400), (50, 100), (150, 0), (450, 0)], [(450, -100), (350, -200), (250, -200)], [(450, -100), (450, 400)]], '{': [[(400, 600), (300, 600), (200, 500), (200, 400), (100, 300), (200, 200), (200, 100), (300, 0), (400, 0)]], '}': [[(100, 0), (200, 0), (300, 100), (300, 200), (400, 300), (300, 400), (300, 500), (200, 600), (100, 600)]]} +MaxHeight = 700 +MaxWidth = 500 + +# Each drill stroke is in a box with corners (-400,-400) to (400,400). The center of the box at (0,0) +# is the drill center. +DrillStrokeList = [ \ + [[(-400, -400), (400, 400)], [(400, -400), (-400, 400)], [(-400, 0), (400, 0)]], \ + [[(0, 0), (-400, 0)], [(0, 0), (400, 400), (400, -400), (0, 0)]], \ + [[(0, 0), (400, 0)], [(0, 0), (-400, 400), (-400, -400), (0, 0)]], \ + [[(-400, 0), (400, 0)], [(0, -400), (0, 400)], [(-390, 0), (-388, 39), (-382, 79), (-372, 117), (-358, 154), (-341, 189), (-320, 223), (-296, 254), (-269, 283), (-239, 308), (-206, 331), (-172, 350), (-135, 366), (-98, 378), (-59, 386), (-20, 389), (20, 389), (59, 386), (98, 378), (135, 366), (172, 350), (206, 331), (239, 308), (269, 283), (296, 254), (320, 223), (341, 189), (358, 154), (372, 117), (382, 79), (388, 39), (390, 0), (388, -39), (382, -79), (372, -117), (358, -154), (341, -189), (320, -223), (296, -254), (269, -283), (239, -308), (206, -331), (172, -350), (135, -366), (98, -378), (59, -386), (20, -389), (-20, -389), (-59, -386), (-98, -378), (-135, -366), (-172, -350), (-206, -331), (-239, -308), (-269, -283), (-296, -254), (-320, -223), (-341, -189), (-358, -154), (-372, -117), (-382, -79), (-388, -39), (-390, 0)]], \ + [[(-400, 0), (400, 0)], [(0, -400), (0, 400)], [(-390, 0), (-388, 39), (-382, 79), (-372, 117), (-358, 154), (-341, 189), (-320, 223), (-296, 254), (-269, 283), (-239, 308), (-206, 331), (-172, 350), (-135, 366), (-98, 378), (-59, 386), (-20, 389), (20, 389), (59, 386), (98, 378), (135, 366), (172, 350), (206, 331), (239, 308), (269, 283), (296, 254), (320, 223), (341, 189), (358, 154), (372, 117), (382, 79), (388, 39), (390, 0), (388, -39), (382, -79), (372, -117), (358, -154), (341, -189), (320, -223), (296, -254), (269, -283), (239, -308), (206, -331), (172, -350), (135, -366), (98, -378), (59, -386), (20, -389), (-20, -389), (-59, -386), (-98, -378), (-135, -366), (-172, -350), (-206, -331), (-239, -308), (-269, -283), (-296, -254), (-320, -223), (-341, -189), (-358, -154), (-372, -117), (-382, -79), (-388, -39), (-390, 0)], [(-195, 0), (-193, 28), (-187, 56), (-177, 83), (-163, 108), (-145, 130), (-125, 150), (-102, 166), (-76, 179), (-49, 189), (-21, 194), (7, 195), (35, 192), (63, 185), (89, 173), (113, 159), (135, 140), (154, 119), (170, 95), (182, 70), (190, 42), (194, 14), (194, -14), (190, -42), (182, -70), (170, -95), (154, -119), (135, -140), (113, -159), (89, -173), (63, -185), (35, -192), (7, -195), (-21, -194), (-49, -189), (-76, -179), (-102, -166), (-125, -150), (-145, -130), (-163, -108), (-177, -83), (-187, -56), (-193, -28), (-195, 0)]], \ + [[(-400, 0), (400, 0), (0, 400), (0, -400), (-400, 0)]], \ + [[(-400, 0), (400, 0)], [(0, -400), (0, 400)], [(-390, 390), (390, 390), (390, -390), (-390, -390), (-390, 390)]], \ + [[(-400, 0), (400, 0)], [(0, -400), (0, 400)]], \ + [[(-400, 0), (400, 0)], [(0, -400), (0, 400)], [(-390, 390), (390, 390), (390, -390), (-390, -390), (-390, 390)], [(-195, 195), (195, 195), (195, -195), (-195, -195), (-195, 195)]], \ + [[(-400, 0), (400, 0), (0, -400), (0, 400), (-400, 0)]], \ + [[(-400, -400), (400, 400), (400, -400), (-400, 400), (-400, -400)]], \ + [[(-400, -400), (400, 400), (-400, 400), (400, -400), (-400, -400)]], \ + [[(0, 0), (0, 400), (400, 0), (0, -400), (-400, 0), (0, 400)]], \ + [[(0, -400), (0, 0), (-400, 400), (400, 400), (0, 0)]], \ + [[(400, -400), (-400, 400)], [(-400, -400), (400, 400)]], \ + [[(0, 0), (0, 400)], [(0, 0), (-400, -400), (400, -400), (0, 0)]], \ + [[(0, 0), (0, 400), (400, 400), (400, -400), (-400, -400), (-400, 400), (0, 400)]], \ + [[(-400, -400), (400, 400)], [(400, -400), (-400, 400)], [(0, 400), (0, -400)]], \ + [[(-390, 0), (-388, 39), (-382, 79), (-372, 117), (-358, 154), (-341, 189), (-320, 223), (-296, 254), (-269, 283), (-239, 308), (-206, 331), (-172, 350), (-135, 366), (-98, 378), (-59, 386), (-20, 389), (20, 389), (59, 386), (98, 378), (135, 366), (172, 350), (206, 331), (239, 308), (269, 283), (296, 254), (320, 223), (341, 189), (358, 154), (372, 117), (382, 79), (388, 39), (390, 0), (388, -39), (382, -79), (372, -117), (358, -154), (341, -189), (320, -223), (296, -254), (269, -283), (239, -308), (206, -331), (172, -350), (135, -366), (98, -378), (59, -386), (20, -389), (-20, -389), (-59, -386), (-98, -378), (-135, -366), (-172, -350), (-206, -331), (-239, -308), (-269, -283), (-296, -254), (-320, -223), (-341, -189), (-358, -154), (-372, -117), (-382, -79), (-388, -39), (-390, 0)], [(-400, -400), (400, 400)]], \ + [[(400,400), (0,0), (-400,0), (0,-400), (0,0)]], \ + [[(-400,400), (0,0), (0, -400), (400,0), (0,0)]], \ + [[(-400,-400), (0,0), (0,400), (400,0), (0,0)]], \ + [[(400,-400), (0,0), (0,400), (-400,0), (0,0)]], \ + [[(0,0), (-400,0), (-400,400), (400,400), (400,-400), (-400,-400), (-400,0)]], \ + [[(0,0), (0,-400), (-400,-400), (-400,400), (400,400), (400,-400), (0,-400)]], \ + [[(0,0), (400,0), (400,-400), (-400,-400), (-400,400), (400,400), (400,0)]] ] + +MaxNumDrillTools = len(DrillStrokeList) diff --git a/gerbmerge/tilesearch1.py b/gerbmerge/tilesearch1.py new file mode 100644 index 0000000..59be680 --- /dev/null +++ b/gerbmerge/tilesearch1.py @@ -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 diff --git a/gerbmerge/tilesearch2.py b/gerbmerge/tilesearch2.py new file mode 100644 index 0000000..4ad5479 --- /dev/null +++ b/gerbmerge/tilesearch2.py @@ -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 diff --git a/gerbmerge/tiling.py b/gerbmerge/tiling.py new file mode 100644 index 0000000..8fc83be --- /dev/null +++ b/gerbmerge/tiling.py @@ -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 above(p1,p2): + return p1[1]>p2[1] and p1[0]==p2[0] + +def below(p1,p2): + return p1[1]t.left_edge + and + p.bottom_edget.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_bl[0] \ + and \ + p_bl[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][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 diff --git a/gerbmerge/util.py b/gerbmerge/util.py new file mode 100644 index 0000000..ad5cf84 --- /dev/null +++ b/gerbmerge/util.py @@ -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 diff --git a/misc/gerbmerge.bat b/misc/gerbmerge.bat new file mode 100644 index 0000000..28926c3 --- /dev/null +++ b/misc/gerbmerge.bat @@ -0,0 +1,3 @@ +@echo off +c:\python26\python c:\python26\lib\site-packages\gerbmerge\gerbmerge.py %1 %2 %3 %4 %5 %6 %7 %8 %9 + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b7192ea --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[sdist] +keep_temp=1 + +[install] +record=installed_files diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ea39091 --- /dev/null +++ b/setup.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +import sys +import glob +import os + +from distutils.core import setup, Extension +import distutils.sysconfig + +from gerbmerge.gerbmerge import VERSION_MAJOR, VERSION_MINOR + +if sys.version_info < (2,4,0): + print '*'*73 + print 'GerbMerge version %d.%d requires Python 2.4 or higher' % (VERSION_MAJOR, VERSION_MINOR) + print '*'*73 + sys.exit(1) + +if 0: + for key,val in distutils.sysconfig.get_config_vars().items(): + print key + print '***********************' + print ' ', val + print + print + + sys.exit(0) + +SampleFiles = glob.glob('testdata/*') +DocFiles = glob.glob('doc/*') +AuxFiles = ['COPYING'] + +if sys.platform == 'win32' or ('bdist_wininst' in sys.argv): + #DestLib = distutils.sysconfig.get_config_var('prefix') + #DestDir = os.path.join(DestLib, 'gerbmerge') + #BinDir = DestLib + DestLib = '.' + DestDir = os.path.join(DestLib, 'gerbmerge') + BinFiles = ['misc/gerbmerge.bat'] + BinDir = '.' +else: + DestLib = distutils.sysconfig.get_config_var('LIBPYTHON') + DestDir = os.path.join(DestLib, 'gerbmerge') + BinFiles = ['misc/gerbmerge'] + BinDir = distutils.sysconfig.get_config_var('BINDIR') + + # Create top-level invocation program + fid = file('misc/gerbmerge', 'wt') + fid.write( \ + r"""#!/bin/sh +python %s/site-packages/gerbmerge/gerbmerge.py $* + """ % DestLib) + fid.close() + +dist=setup (name = "gerbmerge", + license = "GPL", + version = "%d.%d" % (VERSION_MAJOR, VERSION_MINOR), + long_description=\ +r"""GerbMerge is a program that combines several Gerber +(i.e., RS274-X) and Excellon files into a single set +of files. This program is useful for combining multiple +printed circuit board layout files into a single job. + +To run the program, invoke the Python interpreter on the +gerbmerge.py file. On Windows, if you installed GerbMerge in +C:/Python24, for example, open a command window (DOS box) +and type: + C:/Python24/gerbmerge.bat + +For more details on installation or running GerbMerge, see the +URL below. +""", + description = "Merge multiple Gerber/Excellon files", + author = "Rugged Circuits LLC", + author_email = "support@ruggedcircuits.com", + url = "http://ruggedcircuits.com/gerbmerge", + packages = ['gerbmerge'], + platforms = ['all'], + data_files = [ (DestDir, AuxFiles), + (os.path.join(DestDir,'testdata'), SampleFiles), + (os.path.join(DestDir,'doc'), DocFiles), + (BinDir, BinFiles) ] +) + +do_fix_perms = 0 +if sys.platform != "win32": + for cmd in dist.commands: + if cmd[:7]=='install': + do_fix_perms = 1 + break + +if do_fix_perms: + # Ensure package files and misc/help files are world readable-searchable. + # Shouldn't Distutils do this for us? + print 'Setting permissions on installed files...', + try: + def fixperms(arg, dirname, names): + os.chmod(dirname, 0755) + for name in names: + fullname = os.path.join(dirname, name) + if os.access(fullname, os.X_OK): + os.chmod(fullname, 0755) + else: + os.chmod(fullname, 0644) + + os.path.walk(DestDir, fixperms, 1) + os.path.walk(os.path.join(DestLib, 'site-packages/gerbmerge'), fixperms, 1) + + os.chmod(os.path.join(BinDir, 'gerbmerge'), 0755) + print 'done' + except: + print 'FAILED' + print + print '*** Please verify that the installed files have correct permissions. On' + print "*** systems without permission flags, you don't need to" + print '*** worry about it.' + +if cmd[:7]=='install': + print + print '******** Installation Complete ******** ' + print + print 'Sample files and documentation have been installed in:' + print ' ', DestDir + print + print 'A shortcut to starting the program has been installed as:' + print ' ', os.path.join(BinDir, 'gerbmerge') + print diff --git a/testdata/Makefile b/testdata/Makefile new file mode 100644 index 0000000..e232d91 --- /dev/null +++ b/testdata/Makefile @@ -0,0 +1,23 @@ +all: merge1.xln merge2.xln + +merge1.xln: + gerbmerge layout1.cfg layout1.def + +merge2.xln: + gerbmerge layout2.cfg layout2.def + +view1: + gerbv merge1.* & + +view2: + gerbv merge2.* & + +clean: + -rm -f merge1.* merge2.* toollist.* placement.* + +# Demonstrates randomized search +random: + -rm -f merge2.* + -gerbmerge layout2.cfg + +# vim: noexpandtab diff --git a/testdata/README b/testdata/README new file mode 100644 index 0000000..4087083 --- /dev/null +++ b/testdata/README @@ -0,0 +1,30 @@ +This directory contains example projects for demonstrating GerbMerge. + +Proj1 is a small board that has ovals, thermals, arcs, circles, polygons, +and text. Hexapod is the example board that comes with Eagle. + +Each project has CAM data generated from its associated *.cam file, +hence proj1.cam and hexapod.cam were used to generate drawing data. + +The "layout1.cfg" and "layout1.def" files demonstrate how GerbMerge can +be used to panelize a single job, including rotation. + +The "layout2.cfg" and "layout2.def" files demonstrate panelizing +different jobs. + +The Makefile in this directory shows an example of how to invoke +the program and how to view the results under Linux. You need to +have the gerbv program installed (see http://gerbv.sourceforge.net) +for viewing the output files. + +The syntax for running GerbMerge is: + + gerbmerge layout1.cfg layout1.def + +If you are running Linux and have installed the 'gerbv' Gerber previewer, +you can view the results by typing: + + gerbv *.ger *.xln + +On Windows systems, GC-Prevue and ViewMate should be able to Import +the above files for previewing. diff --git a/testdata/fabdwg.txt b/testdata/fabdwg.txt new file mode 100644 index 0000000..3474362 --- /dev/null +++ b/testdata/fabdwg.txt @@ -0,0 +1,7 @@ +* FR4 Material +* 1 oz. copper +* All holes plated through +* Drills shown as finished sizes +* Board thickness 0.062" + + diff --git a/testdata/hexapod.bor b/testdata/hexapod.bor new file mode 100644 index 0000000..b456bcd --- /dev/null +++ b/testdata/hexapod.bor @@ -0,0 +1,157 @@ +G75* +G70* +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0000*% +D10* +X000101Y000252D02* +X039471Y000252D01* +X039471Y031748D01* +X000101Y031748D01* +X000101Y000252D01* +X043351Y020502D02* +X043354Y020561D01* +X043362Y020620D01* +X043375Y020677D01* +X043394Y020734D01* +X043417Y020788D01* +X043446Y020840D01* +X043479Y020889D01* +X043516Y020935D01* +X043557Y020977D01* +X043602Y021015D01* +X043651Y021050D01* +X043702Y021079D01* +X043756Y021104D01* +X043811Y021124D01* +X043869Y021138D01* +X043927Y021148D01* +X043986Y021152D01* +X044045Y021150D01* +X044104Y021144D01* +X044162Y021132D01* +X044219Y021114D01* +X044273Y021092D01* +X044326Y021065D01* +X044376Y021033D01* +X044423Y020997D01* +X044466Y020956D01* +X044505Y020912D01* +X044540Y020865D01* +X044571Y020814D01* +X044597Y020761D01* +X044618Y020706D01* +X044634Y020649D01* +X044645Y020591D01* +X044650Y020532D01* +X044650Y020472D01* +X044645Y020413D01* +X044634Y020355D01* +X044618Y020298D01* +X044597Y020243D01* +X044571Y020190D01* +X044540Y020139D01* +X044505Y020092D01* +X044466Y020048D01* +X044423Y020007D01* +X044376Y019971D01* +X044326Y019939D01* +X044273Y019912D01* +X044219Y019890D01* +X044162Y019872D01* +X044104Y019860D01* +X044045Y019854D01* +X043986Y019852D01* +X043927Y019856D01* +X043869Y019866D01* +X043811Y019880D01* +X043756Y019900D01* +X043702Y019925D01* +X043651Y019954D01* +X043602Y019989D01* +X043557Y020027D01* +X043516Y020069D01* +X043479Y020115D01* +X043446Y020164D01* +X043417Y020216D01* +X043394Y020270D01* +X043375Y020327D01* +X043362Y020384D01* +X043354Y020443D01* +X043351Y020502D01* +X043351Y025002D02* +X043354Y025061D01* +X043362Y025120D01* +X043375Y025177D01* +X043394Y025234D01* +X043417Y025288D01* +X043446Y025340D01* +X043479Y025389D01* +X043516Y025435D01* +X043557Y025477D01* +X043602Y025515D01* +X043651Y025550D01* +X043702Y025579D01* +X043756Y025604D01* +X043811Y025624D01* +X043869Y025638D01* +X043927Y025648D01* +X043986Y025652D01* +X044045Y025650D01* +X044104Y025644D01* +X044162Y025632D01* +X044219Y025614D01* +X044273Y025592D01* +X044326Y025565D01* +X044376Y025533D01* +X044423Y025497D01* +X044466Y025456D01* +X044505Y025412D01* +X044540Y025365D01* +X044571Y025314D01* +X044597Y025261D01* +X044618Y025206D01* +X044634Y025149D01* +X044645Y025091D01* +X044650Y025032D01* +X044650Y024972D01* +X044645Y024913D01* +X044634Y024855D01* +X044618Y024798D01* +X044597Y024743D01* +X044571Y024690D01* +X044540Y024639D01* +X044505Y024592D01* +X044466Y024548D01* +X044423Y024507D01* +X044376Y024471D01* +X044326Y024439D01* +X044273Y024412D01* +X044219Y024390D01* +X044162Y024372D01* +X044104Y024360D01* +X044045Y024354D01* +X043986Y024352D01* +X043927Y024356D01* +X043869Y024366D01* +X043811Y024380D01* +X043756Y024400D01* +X043702Y024425D01* +X043651Y024454D01* +X043602Y024489D01* +X043557Y024527D01* +X043516Y024569D01* +X043479Y024615D01* +X043446Y024664D01* +X043417Y024716D01* +X043394Y024770D01* +X043375Y024827D01* +X043362Y024884D01* +X043354Y024943D01* +X043351Y025002D01* +M02* diff --git a/testdata/hexapod.brd b/testdata/hexapod.brd new file mode 100644 index 0000000..28a0e98 Binary files /dev/null and b/testdata/hexapod.brd differ diff --git a/testdata/hexapod.cam b/testdata/hexapod.cam new file mode 100644 index 0000000..ed2b766 --- /dev/null +++ b/testdata/hexapod.cam @@ -0,0 +1,96 @@ +[CAM Processor Job] +Description="" +Section=Sec_1 +Section=Sec_2 +Section=Sec_3 +Section=Sec_4 +Section=Sec_5 + +[Sec_1] +Name="TopLayer" +Prompt="" +Device="GERBER_RS274X" +Wheel="" +Rack="" +Scale=1.000000 +Output="hexapod.cmp" +Flags="0 0 0 1 0 1 1" +Emulate="0 0 0" +Offset="0.0mil 0.0mil" +Sheet=1 +Tolerance="0.000000 0.000000 0.000000 0.000000 0.000000 0.000000" +Pen="0.0mil 0.000000" +Page="12000.0mil 8000.0mil" +Layers=" 1 17 18 50 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 252 253 255" +Colors=" 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 6 6 4 8 8 8 8 8 8 8 8 8 8 8 8 8 4 4 1 1 1 1 3 3 1 2 6 8 8 5 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 2 4 3 6 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0" + +[Sec_2] +Name="BottomLayer" +Prompt="" +Device="GERBER_RS274X" +Wheel="" +Rack="" +Scale=1.000000 +Output="hexapod.sol" +Flags="0 0 0 1 0 1 1" +Emulate="0 0 0" +Offset="0.0mil 0.0mil" +Sheet=1 +Tolerance="0.000000 0.000000 0.000000 0.000000 0.000000 0.000000" +Pen="0.0mil 0.000000" +Page="12000.0mil 8000.0mil" +Layers=" 16 17 18 50 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 252 253 255" +Colors=" 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 6 6 4 8 8 8 8 8 8 8 8 8 8 8 8 8 4 4 1 1 1 1 3 3 1 2 6 8 8 5 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 2 4 3 6 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0" + +[Sec_3] +Name="BoardOutline" +Prompt="" +Device="GERBER_RS274X" +Wheel="" +Rack="" +Scale=1.000000 +Output="hexapod.bor" +Flags="0 0 0 1 0 1 1" +Emulate="0 0 0" +Offset="0.0mil 0.0mil" +Sheet=1 +Tolerance="0.000000 0.000000 0.000000 0.000000 0.000000 0.000000" +Pen="0.0mil 0.000000" +Page="12000.0mil 8000.0mil" +Layers=" 20 50 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 252 253 255" +Colors=" 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 6 6 4 8 8 8 8 8 8 8 8 8 8 8 8 8 4 4 1 1 1 1 3 3 1 2 6 8 8 5 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 2 4 3 6 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0" + +[Sec_4] +Name="Drills" +Prompt="" +Device="EXCELLON" +Wheel="" +Scale=1.000000 +Output="hexapod.xln" +Flags="0 0 0 1 0 1 1" +Emulate="0 0 0" +Offset="0.0mil 0.0mil" +Sheet=1 +Tolerance="0.000000 0.000000 0.000000 0.000000 0.020000 0.100000" +Pen="0.0mil 0.000000" +Page="12000.0mil 8000.0mil" +Layers=" 44 45 50 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 252 253 255" +Colors=" 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 6 6 4 8 8 8 8 8 8 8 8 8 8 8 8 8 4 4 1 1 1 1 3 3 1 2 6 8 8 5 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 2 4 3 6 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0" + +[Sec_5] +Name="Top Silkscreen" +Prompt="" +Device="GERBER_RS274X" +Wheel="" +Rack="" +Scale=1.000000 +Output="hexapod.plc" +Flags="0 0 0 1 0 1 1" +Emulate="0 0 0" +Offset="0.0mil 0.0mil" +Sheet=1 +Tolerance="0.000000 0.000000 0.000000 0.000000 0.020000 0.100000" +Pen="0.0mil 0.000000" +Page="12000.0mil 8000.0mil" +Layers=" 21 25 50 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 252 253 255" +Colors=" 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 6 6 4 8 8 8 8 8 8 8 8 8 8 8 8 8 4 4 1 1 1 1 3 3 1 2 6 8 8 5 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 2 4 3 6 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0" diff --git a/testdata/hexapod.cmp b/testdata/hexapod.cmp new file mode 100644 index 0000000..7eeff90 --- /dev/null +++ b/testdata/hexapod.cmp @@ -0,0 +1,1096 @@ +G75* +G70* +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0070*% +%ADD11C,0.0630*% +%ADD12C,0.1240*% +%ADD13C,0.0700*% +%ADD14O,0.1575X0.0787*% +%ADD15C,0.0850*% +%ADD16C,0.0160*% +%ADD17C,0.0472*% +%ADD18C,0.0500*% +%ADD19C,0.1000*% +D10* +X001596Y013287D02* +X001596Y013917D01* +X001386Y013917D02* +X001806Y013917D01* +X002030Y013812D02* +X002030Y013392D01* +X002135Y013287D01* +X002346Y013287D01* +X002451Y013392D01* +X002451Y013812D01* +X002346Y013917D01* +X002135Y013917D01* +X002030Y013812D01* +X002675Y013917D02* +X002990Y013917D01* +X003095Y013812D01* +X003095Y013602D01* +X002990Y013497D01* +X002675Y013497D01* +X002675Y013287D02* +X002675Y013917D01* +X005636Y029037D02* +X005636Y029667D01* +X005951Y029457D02* +X005636Y029247D01* +X005951Y029037D01* +X006173Y029037D02* +X006383Y029037D01* +X006278Y029037D02* +X006278Y029667D01* +X006173Y029667D01* +X006603Y029352D02* +X006708Y029457D01* +X007023Y029457D01* +X006918Y029247D02* +X006708Y029247D01* +X006603Y029352D01* +X006603Y029037D02* +X006918Y029037D01* +X007023Y029142D01* +X006918Y029247D01* +X007892Y029142D02* +X008312Y029562D01* +X008312Y029142D01* +X008207Y029037D01* +X007997Y029037D01* +X007892Y029142D01* +X007892Y029562D01* +X007997Y029667D01* +X008207Y029667D01* +X008312Y029562D01* +X008536Y029457D02* +X008746Y029667D01* +X008746Y029037D01* +X008536Y029037D02* +X008956Y029037D01* +X009181Y029037D02* +X009601Y029667D01* +X009825Y029457D02* +X010035Y029667D01* +X010035Y029037D01* +X009825Y029037D02* +X010246Y029037D01* +X010470Y029142D02* +X010575Y029037D01* +X010785Y029037D01* +X010890Y029142D01* +X010890Y029562D01* +X010785Y029667D01* +X010575Y029667D01* +X010470Y029562D01* +X010470Y029457D01* +X010575Y029352D01* +X010890Y029352D01* +X011114Y029457D02* +X011219Y029352D01* +X011535Y029352D01* +X011535Y029142D02* +X011535Y029562D01* +X011429Y029667D01* +X011219Y029667D01* +X011114Y029562D01* +X011114Y029457D01* +X011114Y029142D02* +X011219Y029037D01* +X011429Y029037D01* +X011535Y029142D01* +X011759Y029142D02* +X011864Y029037D01* +X012074Y029037D01* +X012179Y029142D01* +X012179Y029562D01* +X012074Y029667D01* +X011864Y029667D01* +X011759Y029562D01* +X011759Y029457D01* +X011864Y029352D01* +X012179Y029352D01* +X012076Y030287D02* +X011971Y030392D01* +X011971Y030812D01* +X011866Y030707D02* +X012076Y030707D01* +X012296Y030602D02* +X012401Y030707D01* +X012611Y030707D01* +X012716Y030602D01* +X012716Y030497D01* +X012296Y030497D01* +X012296Y030392D02* +X012296Y030602D01* +X012296Y030392D02* +X012401Y030287D01* +X012611Y030287D01* +X012940Y030287D02* +X012940Y030707D01* +X012940Y030497D02* +X013151Y030707D01* +X013256Y030707D01* +X013478Y030602D02* +X013688Y030602D01* +X013583Y030812D02* +X013583Y030287D01* +X013907Y030392D02* +X014012Y030497D01* +X014328Y030497D01* +X014328Y030602D02* +X014328Y030287D01* +X014012Y030287D01* +X013907Y030392D01* +X014012Y030707D02* +X014222Y030707D01* +X014328Y030602D01* +X014552Y030602D02* +X014552Y030392D01* +X014657Y030287D01* +X014972Y030287D01* +X015196Y030392D02* +X015196Y030602D01* +X015301Y030707D01* +X015511Y030707D01* +X015617Y030602D01* +X015617Y030497D01* +X015196Y030497D01* +X015196Y030392D02* +X015301Y030287D01* +X015511Y030287D01* +X014972Y030707D02* +X014657Y030707D01* +X014552Y030602D01* +X013688Y030917D02* +X013583Y030812D01* +X011642Y030602D02* +X011642Y030287D01* +X011642Y030602D02* +X011537Y030707D01* +X011222Y030707D01* +X011222Y030287D01* +X011002Y030287D02* +X010792Y030287D01* +X010897Y030287D02* +X010897Y030917D01* +X010792Y030917D02* +X011002Y030917D01* +X009923Y030917D02* +X009923Y030287D01* +X009608Y030287D01* +X009503Y030392D01* +X009503Y030602D01* +X009608Y030707D01* +X009923Y030707D01* +X009279Y030602D02* +X009279Y030392D01* +X009174Y030287D01* +X008964Y030287D01* +X008858Y030392D01* +X008858Y030602D01* +X008964Y030707D01* +X009174Y030707D01* +X009279Y030602D01* +X008634Y030602D02* +X008634Y030392D01* +X008529Y030287D01* +X008214Y030287D01* +X008214Y030077D02* +X008214Y030707D01* +X008529Y030707D01* +X008634Y030602D01* +X007990Y030602D02* +X007990Y030287D01* +X007674Y030287D01* +X007569Y030392D01* +X007674Y030497D01* +X007990Y030497D01* +X007990Y030602D02* +X007885Y030707D01* +X007674Y030707D01* +X007345Y030707D02* +X006925Y030287D01* +X006701Y030497D02* +X006701Y030602D01* +X006596Y030707D01* +X006385Y030707D01* +X006280Y030602D01* +X006280Y030392D01* +X006385Y030287D01* +X006596Y030287D01* +X006701Y030497D02* +X006280Y030497D01* +X006056Y030602D02* +X005636Y030602D01* +X005636Y030287D02* +X005636Y030917D01* +X006056Y030917D02* +X006056Y030287D01* +X006925Y030707D02* +X007345Y030287D01* +D11* +X001851Y003002D03* +X002851Y003002D03* +X002851Y004002D03* +X001851Y004002D03* +X001851Y005002D03* +X002851Y005002D03* +X002851Y006002D03* +X001851Y006002D03* +X001851Y007002D03* +X002851Y007002D03* +X002851Y008002D03* +X001851Y008002D03* +X001851Y009002D03* +X002851Y009002D03* +X002851Y010002D03* +X001851Y010002D03* +X005601Y012502D03* +X005601Y013502D03* +X005851Y015502D03* +X006851Y015502D03* +X007851Y015502D03* +X008851Y015502D03* +X009851Y015502D03* +X010851Y015502D03* +X011851Y015502D03* +X012851Y015502D03* +X014351Y015502D03* +X015351Y015502D03* +X016351Y015502D03* +X017351Y015502D03* +X018351Y015502D03* +X019351Y015502D03* +X020351Y015502D03* +X021351Y015502D03* +X022851Y015502D03* +X023851Y015502D03* +X024851Y015502D03* +X025851Y015502D03* +X026851Y015502D03* +X027851Y015502D03* +X028851Y015502D03* +X029851Y015502D03* +X031351Y013502D03* +X031351Y012502D03* +X031351Y011502D03* +X031351Y010502D03* +X031351Y009502D03* +X031351Y008502D03* +X031351Y007502D03* +X031351Y006502D03* +X031851Y004502D03* +X030851Y004502D03* +X029851Y004502D03* +X028851Y004502D03* +X027851Y004502D03* +X026851Y004502D03* +X025851Y004502D03* +X024851Y004502D03* +X023851Y004502D03* +X022851Y004502D03* +X021851Y004502D03* +X020851Y004502D03* +X019851Y004502D03* +X018851Y004502D03* +X017851Y004502D03* +X016851Y004502D03* +X015851Y004502D03* +X014851Y004502D03* +X013851Y004502D03* +X012851Y004502D03* +X011851Y004502D03* +X010851Y004502D03* +X009851Y004502D03* +X008851Y004502D03* +X007851Y004502D03* +X007851Y003502D03* +X008851Y003502D03* +X009851Y003502D03* +X010851Y003502D03* +X011851Y003502D03* +X012851Y003502D03* +X013851Y003502D03* +X014851Y003502D03* +X015851Y003502D03* +X016851Y003502D03* +X017851Y003502D03* +X018851Y003502D03* +X019851Y003502D03* +X020851Y003502D03* +X021851Y003502D03* +X022851Y003502D03* +X023851Y003502D03* +X024851Y003502D03* +X025851Y003502D03* +X026851Y003502D03* +X027851Y003502D03* +X028851Y003502D03* +X029851Y003502D03* +X030851Y003502D03* +X031851Y003502D03* +X034351Y001502D03* +X035351Y001502D03* +X036351Y001502D03* +X037351Y001502D03* +X038351Y001502D03* +X038351Y005502D03* +X037351Y005502D03* +X036351Y005502D03* +X035351Y005502D03* +X034351Y005502D03* +X036351Y008252D03* +X037351Y008252D03* +X037351Y009252D03* +X036351Y009252D03* +X036351Y010252D03* +X037351Y010252D03* +X037351Y011252D03* +X036351Y011252D03* +X036351Y012252D03* +X037351Y012252D03* +X037351Y013252D03* +X036351Y013252D03* +X036351Y014252D03* +X037351Y014252D03* +X037351Y015252D03* +X036351Y015252D03* +X032851Y014752D03* +X032851Y015752D03* +X031851Y017502D03* +X031851Y018502D03* +X029851Y018502D03* +X028851Y018502D03* +X027851Y018502D03* +X026851Y018502D03* +X025851Y018502D03* +X024851Y018502D03* +X023851Y018502D03* +X022851Y018502D03* +X021351Y018502D03* +X020351Y018502D03* +X019351Y018502D03* +X018351Y018502D03* +X017351Y018502D03* +X016351Y018502D03* +X015351Y018502D03* +X014351Y018502D03* +X012851Y018502D03* +X011851Y018502D03* +X010851Y018502D03* +X009851Y018502D03* +X008851Y018502D03* +X007851Y018502D03* +X006851Y018502D03* +X005851Y018502D03* +X002851Y019002D03* +X001851Y019002D03* +X001851Y020002D03* +X002851Y020002D03* +X002851Y021002D03* +X001851Y021002D03* +X001851Y022002D03* +X002851Y022002D03* +X002851Y023002D03* +X001851Y023002D03* +X001851Y024002D03* +X002851Y024002D03* +X002851Y025002D03* +X001851Y025002D03* +X001851Y026002D03* +X002851Y026002D03* +X002851Y027002D03* +X001851Y027002D03* +X001851Y028002D03* +X002851Y028002D03* +X002851Y029002D03* +X001851Y029002D03* +X007851Y022502D03* +X008851Y022502D03* +X009851Y022502D03* +X010851Y022502D03* +X011851Y022502D03* +X012851Y022502D03* +X013851Y022502D03* +X014851Y022502D03* +X015851Y022502D03* +X016851Y022502D03* +X017851Y022502D03* +X018851Y022502D03* +X019851Y022502D03* +X020851Y022502D03* +X021851Y022502D03* +X022851Y022502D03* +X023851Y022502D03* +X024851Y022502D03* +X025851Y022502D03* +X026851Y022502D03* +X027851Y022502D03* +X028851Y022502D03* +X029851Y022502D03* +X030851Y022502D03* +X031851Y022502D03* +X031851Y021502D03* +X030851Y021502D03* +X029851Y021502D03* +X028851Y021502D03* +X027851Y021502D03* +X026851Y021502D03* +X025851Y021502D03* +X024851Y021502D03* +X023851Y021502D03* +X022851Y021502D03* +X021851Y021502D03* +X020851Y021502D03* +X019851Y021502D03* +X018851Y021502D03* +X017851Y021502D03* +X016851Y021502D03* +X015851Y021502D03* +X014851Y021502D03* +X013851Y021502D03* +X012851Y021502D03* +X011851Y021502D03* +X010851Y021502D03* +X009851Y021502D03* +X008851Y021502D03* +X007851Y021502D03* +X002851Y018002D03* +X001851Y018002D03* +X001851Y017002D03* +X002851Y017002D03* +X008351Y013502D03* +X008351Y012502D03* +X008351Y011502D03* +X008351Y010502D03* +X008351Y009502D03* +X008351Y008502D03* +X008351Y007502D03* +X008351Y006502D03* +X005601Y006502D03* +X005601Y007502D03* +X011351Y007502D03* +X012351Y007502D03* +X012351Y008502D03* +X012351Y009502D03* +X012351Y010502D03* +X011351Y010502D03* +X011351Y009502D03* +X011351Y008502D03* +X011351Y006502D03* +X012351Y006502D03* +X015351Y006502D03* +X016351Y006502D03* +X016351Y007502D03* +X015351Y007502D03* +X015351Y008502D03* +X016351Y008502D03* +X016351Y009502D03* +X015351Y009502D03* +X015351Y010502D03* +X016351Y010502D03* +X016351Y011502D03* +X015351Y011502D03* +X015351Y012502D03* +X016351Y012502D03* +X016351Y013502D03* +X015351Y013502D03* +X012351Y013502D03* +X012351Y012502D03* +X012351Y011502D03* +X011351Y011502D03* +X011351Y012502D03* +X011351Y013502D03* +X019351Y013502D03* +X020351Y013502D03* +X020351Y012502D03* +X019351Y012502D03* +X019351Y011502D03* +X020351Y011502D03* +X020351Y010502D03* +X019351Y010502D03* +X019351Y009502D03* +X020351Y009502D03* +X020351Y008502D03* +X019351Y008502D03* +X019351Y007502D03* +X020351Y007502D03* +X020351Y006502D03* +X019351Y006502D03* +X023351Y006502D03* +X024351Y006502D03* +X024351Y007502D03* +X023351Y007502D03* +X023351Y008502D03* +X024351Y008502D03* +X024351Y009502D03* +X023351Y009502D03* +X023351Y010502D03* +X024351Y010502D03* +X024351Y011502D03* +X023351Y011502D03* +X023351Y012502D03* +X024351Y012502D03* +X024351Y013502D03* +X023351Y013502D03* +X027351Y013502D03* +X028351Y013502D03* +X028351Y012502D03* +X028351Y011502D03* +X027351Y011502D03* +X027351Y012502D03* +X027351Y010502D03* +X028351Y010502D03* +X028351Y009502D03* +X028351Y008502D03* +X028351Y007502D03* +X028351Y006502D03* +X027351Y006502D03* +X027351Y007502D03* +X027351Y008502D03* +X027351Y009502D03* +X021601Y028752D03* +X021601Y029752D03* +D12* +X020351Y027502D03* +X025351Y029502D03* +X029291Y029502D03* +X032241Y029502D03* +X035191Y029502D03* +X006351Y027502D03* +D13* +X030351Y026252D03* +X030351Y024252D03* +D14* +X037101Y024002D03* +X037101Y025002D03* +X037101Y026002D03* +X037101Y021502D03* +X037101Y020502D03* +X037101Y019502D03* +D15* +X032851Y013502D03* +X032851Y009502D03* +D16* +X032851Y001752D01* +X033101Y001502D01* +X034351Y001502D01* +X033851Y002252D02* +X033851Y011752D01* +X033601Y012002D01* +X031851Y012002D01* +X031351Y012502D01* +X031851Y013002D02* +X031851Y014002D01* +X031101Y014752D01* +X031101Y018252D01* +X031351Y018502D01* +X031851Y018502D01* +X030851Y019752D02* +X031351Y020252D01* +X031351Y022002D01* +X031851Y022502D01* +X030351Y022002D02* +X030351Y020252D01* +X030601Y020002D01* +X030601Y009752D01* +X030351Y009502D01* +X029851Y010252D02* +X030351Y010752D01* +X030351Y019752D01* +X029351Y020752D01* +X029351Y023252D01* +X029101Y023502D01* +X028351Y023502D01* +X028851Y022502D02* +X028351Y022002D01* +X028351Y020752D01* +X027351Y019752D01* +X027351Y014752D01* +X026601Y014002D01* +X026601Y009752D01* +X026351Y009502D01* +X026101Y009502D01* +X027351Y009502D02* +X027851Y010002D01* +X027851Y014252D01* +X028351Y014752D01* +X028351Y016502D01* +X029351Y017502D01* +X029351Y020252D01* +X028851Y020752D01* +X028851Y021502D01* +X029851Y022502D02* +X030351Y022002D01* +X030851Y019752D02* +X030851Y008002D01* +X031351Y007502D01* +X029851Y007252D02* +X031601Y005502D01* +X032351Y005502D01* +X032601Y005252D01* +X032601Y000752D01* +X028601Y000752D01* +X028351Y001002D01* +X028351Y004002D01* +X027851Y004502D01* +X027351Y004002D02* +X027351Y001252D01* +X027101Y001002D01* +X007351Y001002D01* +X007101Y001252D01* +X007101Y007002D01* +X007351Y007252D01* +X007351Y010002D01* +X007101Y010252D01* +X007101Y014502D01* +X006851Y014752D01* +X006851Y015502D01* +X007351Y016252D02* +X007101Y016502D01* +X004601Y016502D01* +X004351Y016752D01* +X004351Y023002D01* +X004601Y023252D01* +X006351Y023252D01* +X004351Y025252D02* +X004101Y025002D01* +X004101Y006252D01* +X003851Y006002D01* +X002851Y006002D01* +X002351Y006502D02* +X001851Y006002D01* +X002351Y006502D02* +X003601Y006502D01* +X003851Y006752D01* +X003851Y025752D01* +X004101Y026002D01* +X006351Y026002D01* +X006351Y025252D02* +X004351Y025252D01* +X009351Y023752D02* +X009351Y020502D01* +X008351Y019502D01* +X008351Y014752D01* +X009101Y014002D01* +X009101Y010752D01* +X008851Y010502D01* +X008351Y010502D01* +X008351Y009502D02* +X007351Y010502D01* +X007351Y016252D01* +X005851Y015502D02* +X005851Y014752D01* +X006851Y013752D01* +X006851Y010002D01* +X007101Y009752D01* +X007101Y007502D01* +X006851Y007252D01* +X006851Y001002D01* +X007101Y000752D01* +X027601Y000752D01* +X027851Y001002D01* +X027851Y003502D01* +X027351Y004002D02* +X026851Y004502D01* +X026351Y004002D02* +X026351Y001752D01* +X026101Y001502D01* +X012601Y001502D01* +X012351Y001752D01* +X012351Y005002D01* +X013351Y006002D01* +X013351Y014252D01* +X012851Y014752D01* +X012851Y015502D01* +X012351Y014752D02* +X012351Y016502D01* +X013351Y017502D01* +X013351Y019502D01* +X014351Y020502D01* +X014351Y023252D01* +X014851Y022502D02* +X014851Y028502D01* +X015101Y028752D01* +X021601Y028752D01* +X021601Y029752D02* +X014101Y029752D01* +X013851Y029502D01* +X013851Y022502D01* +X013351Y020502D02* +X012351Y019502D01* +X012351Y017502D01* +X011351Y016502D01* +X011351Y014752D01* +X010601Y014002D01* +X010601Y009752D01* +X010851Y009502D01* +X011351Y009502D01* +X010351Y008752D02* +X010601Y008502D01* +X011351Y008502D01* +X010351Y008752D02* +X010351Y019502D01* +X011351Y020502D01* +X011351Y026002D01* +X012351Y026502D02* +X012351Y020502D01* +X011351Y019502D01* +X011351Y017002D01* +X010851Y016502D01* +X010851Y015502D01* +X011851Y015502D02* +X011851Y005752D01* +X011351Y005252D01* +X011351Y001502D01* +X011601Y001252D01* +X026601Y001252D01* +X026851Y001502D01* +X026851Y003502D01* +X026351Y004002D02* +X025851Y004502D01* +X025351Y004002D02* +X025351Y002252D01* +X025101Y002002D01* +X016851Y002002D01* +X016351Y002502D01* +X016351Y005252D01* +X017101Y006002D01* +X017101Y013752D01* +X015351Y015502D01* +X014351Y015502D02* +X015851Y014002D01* +X015851Y005752D01* +X015351Y005252D01* +X015351Y002002D01* +X015601Y001752D01* +X025601Y001752D01* +X025851Y002002D01* +X025851Y003502D01* +X025351Y004002D02* +X024851Y004502D01* +X024351Y004002D02* +X024351Y002752D01* +X024101Y002502D01* +X021601Y002502D01* +X021351Y002752D01* +X021351Y005502D01* +X021601Y005752D01* +X021601Y012252D01* +X021351Y012502D01* +X021351Y015502D01* +X020851Y014752D02* +X020851Y019002D01* +X019351Y020502D01* +X019351Y023252D01* +X018601Y024002D01* +X018601Y027252D01* +X019101Y027752D02* +X019101Y024252D01* +X019351Y024002D01* +X022101Y024002D01* +X022351Y023752D01* +X022351Y013252D01* +X021851Y012752D01* +X021851Y013502D02* +X021851Y021502D01* +X022851Y021502D02* +X022851Y020502D01* +X023351Y020002D01* +X023351Y014752D01* +X023851Y014252D01* +X023851Y012002D01* +X024351Y011502D01* +X026101Y010502D02* +X026101Y014502D01* +X026351Y014752D01* +X026351Y021002D01* +X025851Y021502D01* +X025351Y022002D02* +X025351Y014752D01* +X025601Y014502D01* +X025601Y008252D01* +X025851Y008002D01* +X026101Y008002D01* +X025351Y007252D02* +X025601Y007002D01* +X026101Y007002D01* +X025351Y007252D02* +X025351Y014002D01* +X024351Y015002D01* +X024351Y020002D01* +X024851Y020502D01* +X024851Y021502D01* +X025351Y022002D02* +X024851Y022502D01* +X028351Y019752D02* +X028351Y017002D01* +X027851Y016502D01* +X027851Y015502D01* +X028851Y015502D02* +X028851Y005752D01* +X028601Y005502D01* +X023601Y005502D01* +X023351Y005252D01* +X023351Y004002D01* +X022851Y003502D01* +X022351Y003002D02* +X022601Y002752D01* +X023601Y002752D01* +X023851Y003002D01* +X023851Y003502D01* +X024351Y004002D02* +X023851Y004502D01* +X022851Y004502D02* +X022851Y005502D01* +X023101Y005752D01* +X024851Y005752D01* +X025101Y006002D01* +X025101Y013752D01* +X023851Y015002D01* +X023851Y015502D01* +X022851Y015502D02* +X022851Y014752D01* +X022601Y014502D01* +X022601Y005752D01* +X022351Y005502D01* +X022351Y003002D01* +X020851Y002252D02* +X024601Y002252D01* +X024851Y002502D01* +X024851Y003502D01* +X020851Y002252D02* +X020351Y002752D01* +X020351Y005252D01* +X019851Y005752D01* +X019851Y014252D01* +X020351Y014752D01* +X020351Y015502D01* +X020851Y014752D02* +X021101Y014502D01* +X021101Y012002D01* +X018601Y013752D02* +X017851Y014502D01* +X017851Y020002D01* +X017351Y020502D01* +X017351Y023252D01* +X016851Y023752D01* +X016851Y025502D01* +X016351Y026002D02* +X016351Y020502D01* +X014851Y019002D01* +X014851Y017502D01* +X013851Y016502D01* +X013851Y015002D01* +X014601Y014252D01* +X014601Y009752D01* +X014851Y009502D01* +X015351Y009502D01* +X014101Y011502D02* +X014101Y005752D01* +X014351Y005502D01* +X014351Y002002D01* +X013351Y002002D02* +X013351Y005502D01* +X013601Y005752D01* +X013601Y012252D01* +X013851Y012502D01* +X014101Y012502D01* +X013101Y012752D02* +X012851Y012502D01* +X012351Y012502D01* +X013101Y012752D02* +X013101Y014002D01* +X012351Y014752D01* +X017351Y012252D02* +X017351Y002502D01* +X018351Y002502D02* +X018351Y005502D01* +X018101Y005752D01* +X018101Y011502D01* +X017351Y012252D02* +X017601Y012502D01* +X018101Y012502D01* +X018601Y013752D02* +X018601Y009752D01* +X018851Y009502D01* +X019351Y009502D01* +X011351Y006502D02* +X009601Y006502D01* +X009351Y006752D01* +X009351Y019502D01* +X010351Y020502D01* +X010351Y025002D01* +X013351Y025502D02* +X013351Y020502D01* +X018351Y020502D02* +X018351Y023252D01* +X017351Y024252D01* +X017351Y026502D01* +X018351Y020502D02* +X018851Y020002D01* +X018851Y017002D01* +X019351Y016502D01* +X019351Y015502D01* +X029101Y014752D02* +X029101Y005752D01* +X029351Y005502D01* +X029351Y002502D01* +X029101Y002252D01* +X030351Y001502D02* +X030351Y005002D01* +X029351Y006002D01* +X029351Y012002D01* +X029851Y012502D01* +X029851Y011502D02* +X029601Y011252D01* +X029601Y007002D01* +X031351Y005252D01* +X031351Y002002D01* +X032601Y000752D02* +X036101Y000752D01* +X036351Y001002D01* +X036351Y001502D01* +X038351Y001502D02* +X038351Y003002D01* +X038101Y003252D01* +X035101Y003252D01* +X034851Y003502D01* +X034851Y012002D01* +X034351Y012502D01* +X032351Y012502D01* +X031851Y013002D01* +X032851Y013502D02* +X035101Y013502D01* +X035351Y013252D01* +X035351Y007502D01* +X035601Y007252D01* +X036101Y007252D01* +X036351Y007002D01* +X036351Y005502D01* +X033601Y003002D02* +X033601Y011252D01* +X033351Y011502D01* +X031351Y011502D01* +X029851Y010252D02* +X029851Y007252D01* +X033601Y003002D02* +X033351Y002752D01* +X029101Y014752D02* +X029851Y015502D01* +X006601Y009002D02* +X004601Y009002D01* +X004351Y008752D01* +X004351Y003752D01* +X004101Y003502D01* +X002351Y003502D01* +X001851Y003002D01* +X002851Y003002D02* +X004351Y003002D01* +X004601Y003252D01* +X004601Y008002D01* +X004851Y008252D01* +X006601Y008252D01* +D17* +X006601Y008252D03* +X006601Y009002D03* +X014101Y011502D03* +X014101Y012502D03* +X018101Y012502D03* +X018101Y011502D03* +X021101Y012002D03* +X021851Y012752D03* +X021851Y013502D03* +X026101Y010502D03* +X026101Y009502D03* +X026101Y008002D03* +X026101Y007002D03* +X030351Y009502D03* +X029851Y011502D03* +X029851Y012502D03* +X028351Y019752D03* +X028351Y023502D03* +X019101Y027752D03* +X018601Y027252D03* +X017351Y026502D03* +X016851Y025502D03* +X016351Y026002D03* +X013351Y025502D03* +X012351Y026502D03* +X011351Y026002D03* +X010351Y025002D03* +X009351Y023752D03* +X006351Y023252D03* +X006351Y025252D03* +X006351Y026002D03* +X014351Y023252D03* +X033351Y002752D03* +X033851Y002252D03* +X031351Y002002D03* +X030351Y001502D03* +X029101Y002252D03* +X018351Y002502D03* +X017351Y002502D03* +X014351Y002002D03* +X013351Y002002D03* +D18* +X031851Y014752D02* +X032851Y014752D01* +X031851Y014752D02* +X031851Y017502D01* +X031851Y018502D02* +X032851Y018502D01* +X032851Y024002D01* +X037101Y024002D01* +X037101Y025002D02* +X030351Y025002D01* +X030351Y024252D01* +X037101Y026002D02* +X037101Y026502D01* +X037101Y019502D02* +X036101Y019502D01* +X026851Y018502D02* +X026851Y015502D01* +X025851Y015502D02* +X025851Y018502D01* +X018351Y018502D02* +X018351Y015502D01* +X017351Y015502D02* +X017351Y018502D01* +X009851Y018502D02* +X009851Y015502D01* +X008851Y015502D02* +X008851Y018502D01* +X007851Y017502D02* +X007851Y015502D01* +X007851Y017502D02* +X004851Y017502D01* +X004851Y021502D01* +X007851Y021502D01* +X007851Y022502D02* +X007851Y023002D01* +D19* +X007851Y027502D01* +X006351Y027502D01* +X020351Y027502D02* +X025351Y027502D01* +X030601Y027502D01* +X030351Y027502D01* +X030351Y026252D01* +X030601Y027502D02* +X037101Y027502D01* +X037101Y026502D01* +X036101Y019502D02* +X034101Y019502D01* +X034101Y015752D01* +X032851Y015752D01* +X025351Y027502D02* +X025351Y029502D01* +M02* diff --git a/testdata/hexapod.plc b/testdata/hexapod.plc new file mode 100644 index 0000000..38292de --- /dev/null +++ b/testdata/hexapod.plc @@ -0,0 +1,3823 @@ +G75* +G70* +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0050*% +%ADD11C,0.0060*% +%ADD12C,0.0070*% +%ADD13C,0.0020*% +%ADD14C,0.0010*% +%ADD15C,0.0120*% +%ADD16R,0.0250X0.0300*% +%ADD17R,0.1100X0.0500*% +%ADD18R,0.0240X0.0340*% +%ADD19R,0.1000X0.0200*% +%ADD20R,0.0300X0.0750*% +%ADD21R,0.0200X0.1200*% +%ADD22R,0.0350X0.0300*% +D10* +X000601Y001002D02* +X004101Y001002D01* +X004101Y001502D01* +X004201Y001502D01* +X004201Y002002D01* +X004101Y002002D01* +X004101Y006252D01* +X004201Y006252D01* +X004201Y006752D01* +X004101Y006752D01* +X004101Y011002D01* +X004201Y011002D01* +X004201Y011502D01* +X004101Y011502D01* +X004101Y012002D01* +X000601Y012002D01* +X000601Y010302D01* +X000651Y010302D01* +X000651Y009702D01* +X000601Y009702D01* +X000601Y009152D01* +X000801Y009002D01* +X000801Y008802D01* +X000801Y008202D01* +X000801Y008002D01* +X000601Y007852D01* +X000601Y007302D01* +X001001Y007302D01* +X001001Y008202D01* +X001101Y008202D01* +X001101Y008802D01* +X001101Y011502D01* +X003601Y011502D01* +X003601Y001502D01* +X001101Y001502D01* +X001101Y003202D01* +X001101Y003802D01* +X001101Y005702D01* +X001001Y005702D01* +X000601Y005702D01* +X000601Y004152D01* +X000801Y004002D01* +X000801Y003802D01* +X000801Y003202D01* +X001001Y003202D01* +X001101Y003202D01* +X000801Y003202D02* +X000801Y003002D01* +X000601Y002852D01* +X000601Y001002D01* +X000801Y003802D02* +X001001Y003802D01* +X001101Y003802D01* +X001101Y005702D02* +X001401Y005702D01* +X001401Y007302D01* +X001101Y007302D01* +X001001Y007302D01* +X001101Y007302D02* +X001101Y008202D01* +X001001Y008202D02* +X000801Y008202D01* +X000801Y008802D02* +X001001Y008802D01* +X001101Y008802D01* +X001001Y008802D02* +X001001Y011602D01* +X005001Y012452D02* +X005101Y012452D01* +X005101Y012552D01* +X005101Y012452D02* +X005201Y012452D01* +X005101Y012452D02* +X005101Y012352D01* +X004601Y013002D02* +X004605Y013093D01* +X004618Y013183D01* +X004638Y013272D01* +X004667Y013358D01* +X004703Y013442D01* +X004747Y013522D01* +X004797Y013597D01* +X004855Y013668D01* +X004918Y013733D01* +X004988Y013792D01* +X005062Y013844D01* +X005141Y013890D01* +X005224Y013928D01* +X005309Y013959D01* +X005398Y013981D01* +X005487Y013996D01* +X005578Y014002D01* +X005669Y014000D01* +X005760Y013989D01* +X005849Y013971D01* +X005936Y013944D01* +X006020Y013910D01* +X006101Y013868D01* +X006178Y013819D01* +X006250Y013763D01* +X006316Y013701D01* +X006377Y013633D01* +X006431Y013560D01* +X006478Y013482D01* +X006518Y013400D01* +X006551Y013315D01* +X006575Y013228D01* +X006592Y013138D01* +X006600Y013048D01* +X006600Y012956D01* +X006592Y012866D01* +X006575Y012776D01* +X006551Y012689D01* +X006518Y012604D01* +X006478Y012522D01* +X006431Y012444D01* +X006377Y012371D01* +X006316Y012303D01* +X006250Y012241D01* +X006178Y012185D01* +X006101Y012136D01* +X006020Y012094D01* +X005936Y012060D01* +X005849Y012033D01* +X005760Y012015D01* +X005669Y012004D01* +X005578Y012002D01* +X005487Y012008D01* +X005398Y012023D01* +X005309Y012045D01* +X005224Y012076D01* +X005141Y012114D01* +X005062Y012160D01* +X004988Y012212D01* +X004918Y012271D01* +X004855Y012336D01* +X004797Y012407D01* +X004747Y012482D01* +X004703Y012562D01* +X004667Y012646D01* +X004638Y012732D01* +X004618Y012821D01* +X004605Y012911D01* +X004601Y013002D01* +X004101Y015002D02* +X004101Y015502D01* +X004201Y015502D01* +X004201Y016002D01* +X004101Y016002D01* +X004101Y022752D01* +X004201Y022752D01* +X004201Y023252D01* +X004101Y023252D01* +X004101Y030002D01* +X004201Y030002D01* +X004201Y030502D01* +X004101Y030502D01* +X004101Y031002D01* +X000601Y031002D01* +X000601Y029302D01* +X000651Y029302D01* +X000651Y028702D01* +X000601Y028702D01* +X000601Y028152D01* +X000801Y028002D01* +X000801Y027802D01* +X000801Y027202D01* +X000801Y027002D01* +X000601Y026852D01* +X000601Y023802D01* +X001001Y023802D01* +X001101Y023802D01* +X001101Y027202D01* +X001101Y027802D01* +X001101Y030502D01* +X003601Y030502D01* +X003601Y015502D01* +X001101Y015502D01* +X001101Y017202D01* +X001101Y017802D01* +X001101Y022202D01* +X001001Y022202D01* +X000601Y022202D01* +X000601Y018152D01* +X000801Y018002D01* +X000801Y017802D01* +X000801Y017202D01* +X001001Y017202D01* +X001101Y017202D01* +X000801Y017202D02* +X000801Y017002D01* +X000601Y016852D01* +X000601Y015002D01* +X004101Y015002D01* +X005351Y015902D02* +X013351Y015902D01* +X013351Y016752D01* +X013320Y016754D01* +X013289Y016760D01* +X013259Y016770D01* +X013231Y016783D01* +X013204Y016800D01* +X013180Y016820D01* +X013158Y016843D01* +X013140Y016868D01* +X013125Y016896D01* +X013113Y016925D01* +X013105Y016955D01* +X013101Y016986D01* +X013101Y017018D01* +X013105Y017049D01* +X013113Y017079D01* +X013125Y017108D01* +X013140Y017136D01* +X013158Y017161D01* +X013180Y017184D01* +X013204Y017204D01* +X013231Y017221D01* +X013259Y017234D01* +X013289Y017244D01* +X013320Y017250D01* +X013351Y017252D01* +X013351Y018102D01* +X005351Y018102D01* +X005351Y015902D01* +X008751Y014002D02* +X008751Y006002D01* +X009601Y006002D01* +X009603Y006033D01* +X009609Y006064D01* +X009619Y006094D01* +X009632Y006122D01* +X009649Y006149D01* +X009669Y006173D01* +X009692Y006195D01* +X009717Y006213D01* +X009745Y006228D01* +X009774Y006240D01* +X009804Y006248D01* +X009835Y006252D01* +X009867Y006252D01* +X009898Y006248D01* +X009928Y006240D01* +X009957Y006228D01* +X009985Y006213D01* +X010010Y006195D01* +X010033Y006173D01* +X010053Y006149D01* +X010070Y006122D01* +X010083Y006094D01* +X010093Y006064D01* +X010099Y006033D01* +X010101Y006002D01* +X010951Y006002D01* +X010951Y014002D01* +X008751Y014002D01* +X012751Y014002D02* +X012751Y006002D01* +X013601Y006002D01* +X013603Y006033D01* +X013609Y006064D01* +X013619Y006094D01* +X013632Y006122D01* +X013649Y006149D01* +X013669Y006173D01* +X013692Y006195D01* +X013717Y006213D01* +X013745Y006228D01* +X013774Y006240D01* +X013804Y006248D01* +X013835Y006252D01* +X013867Y006252D01* +X013898Y006248D01* +X013928Y006240D01* +X013957Y006228D01* +X013985Y006213D01* +X014010Y006195D01* +X014033Y006173D01* +X014053Y006149D01* +X014070Y006122D01* +X014083Y006094D01* +X014093Y006064D01* +X014099Y006033D01* +X014101Y006002D01* +X014951Y006002D01* +X014951Y014002D01* +X012751Y014002D01* +X013851Y015902D02* +X021851Y015902D01* +X021851Y016752D01* +X021820Y016754D01* +X021789Y016760D01* +X021759Y016770D01* +X021731Y016783D01* +X021704Y016800D01* +X021680Y016820D01* +X021658Y016843D01* +X021640Y016868D01* +X021625Y016896D01* +X021613Y016925D01* +X021605Y016955D01* +X021601Y016986D01* +X021601Y017018D01* +X021605Y017049D01* +X021613Y017079D01* +X021625Y017108D01* +X021640Y017136D01* +X021658Y017161D01* +X021680Y017184D01* +X021704Y017204D01* +X021731Y017221D01* +X021759Y017234D01* +X021789Y017244D01* +X021820Y017250D01* +X021851Y017252D01* +X021851Y018102D01* +X013851Y018102D01* +X013851Y015902D01* +X016751Y014002D02* +X016751Y006002D01* +X017601Y006002D01* +X017603Y006033D01* +X017609Y006064D01* +X017619Y006094D01* +X017632Y006122D01* +X017649Y006149D01* +X017669Y006173D01* +X017692Y006195D01* +X017717Y006213D01* +X017745Y006228D01* +X017774Y006240D01* +X017804Y006248D01* +X017835Y006252D01* +X017867Y006252D01* +X017898Y006248D01* +X017928Y006240D01* +X017957Y006228D01* +X017985Y006213D01* +X018010Y006195D01* +X018033Y006173D01* +X018053Y006149D01* +X018070Y006122D01* +X018083Y006094D01* +X018093Y006064D01* +X018099Y006033D01* +X018101Y006002D01* +X018951Y006002D01* +X018951Y014002D01* +X016751Y014002D01* +X020751Y014002D02* +X020751Y006002D01* +X021601Y006002D01* +X021603Y006033D01* +X021609Y006064D01* +X021619Y006094D01* +X021632Y006122D01* +X021649Y006149D01* +X021669Y006173D01* +X021692Y006195D01* +X021717Y006213D01* +X021745Y006228D01* +X021774Y006240D01* +X021804Y006248D01* +X021835Y006252D01* +X021867Y006252D01* +X021898Y006248D01* +X021928Y006240D01* +X021957Y006228D01* +X021985Y006213D01* +X022010Y006195D01* +X022033Y006173D01* +X022053Y006149D01* +X022070Y006122D01* +X022083Y006094D01* +X022093Y006064D01* +X022099Y006033D01* +X022101Y006002D01* +X022951Y006002D01* +X022951Y014002D01* +X020751Y014002D01* +X022351Y015902D02* +X030351Y015902D01* +X030351Y016752D01* +X030320Y016754D01* +X030289Y016760D01* +X030259Y016770D01* +X030231Y016783D01* +X030204Y016800D01* +X030180Y016820D01* +X030158Y016843D01* +X030140Y016868D01* +X030125Y016896D01* +X030113Y016925D01* +X030105Y016955D01* +X030101Y016986D01* +X030101Y017018D01* +X030105Y017049D01* +X030113Y017079D01* +X030125Y017108D01* +X030140Y017136D01* +X030158Y017161D01* +X030180Y017184D01* +X030204Y017204D01* +X030231Y017221D01* +X030259Y017234D01* +X030289Y017244D01* +X030320Y017250D01* +X030351Y017252D01* +X030351Y018102D01* +X022351Y018102D01* +X022351Y015902D01* +X024751Y014002D02* +X024751Y006002D01* +X025601Y006002D01* +X025603Y006033D01* +X025609Y006064D01* +X025619Y006094D01* +X025632Y006122D01* +X025649Y006149D01* +X025669Y006173D01* +X025692Y006195D01* +X025717Y006213D01* +X025745Y006228D01* +X025774Y006240D01* +X025804Y006248D01* +X025835Y006252D01* +X025867Y006252D01* +X025898Y006248D01* +X025928Y006240D01* +X025957Y006228D01* +X025985Y006213D01* +X026010Y006195D01* +X026033Y006173D01* +X026053Y006149D01* +X026070Y006122D01* +X026083Y006094D01* +X026093Y006064D01* +X026099Y006033D01* +X026101Y006002D01* +X026951Y006002D01* +X026951Y014002D01* +X024751Y014002D01* +X028751Y014002D02* +X028751Y006002D01* +X029601Y006002D01* +X029603Y006033D01* +X029609Y006064D01* +X029619Y006094D01* +X029632Y006122D01* +X029649Y006149D01* +X029669Y006173D01* +X029692Y006195D01* +X029717Y006213D01* +X029745Y006228D01* +X029774Y006240D01* +X029804Y006248D01* +X029835Y006252D01* +X029867Y006252D01* +X029898Y006248D01* +X029928Y006240D01* +X029957Y006228D01* +X029985Y006213D01* +X030010Y006195D01* +X030033Y006173D01* +X030053Y006149D01* +X030070Y006122D01* +X030083Y006094D01* +X030093Y006064D01* +X030099Y006033D01* +X030101Y006002D01* +X030951Y006002D01* +X030951Y014002D01* +X028751Y014002D01* +X032351Y012302D02* +X032351Y010702D01* +X033351Y010702D01* +X033351Y012302D01* +X032351Y012302D01* +X032601Y011902D02* +X032851Y011502D01* +X032601Y011502D01* +X032601Y011602D01* +X032851Y011502D02* +X032851Y011252D01* +X032851Y011502D02* +X033101Y011502D01* +X032851Y011502D02* +X032851Y012102D01* +X033101Y011902D02* +X032851Y011502D01* +X032601Y011902D02* +X033101Y011902D01* +X035001Y012002D02* +X035001Y011502D01* +X035101Y011502D01* +X035101Y007252D01* +X035001Y007252D01* +X035001Y006752D01* +X035101Y006752D01* +X035101Y006252D01* +X038601Y006252D01* +X038601Y007952D01* +X038551Y007952D01* +X038551Y008552D01* +X038601Y008552D01* +X038601Y009102D01* +X038401Y009252D01* +X038401Y009452D01* +X038401Y010052D01* +X038401Y010252D01* +X038601Y010402D01* +X038601Y010952D01* +X038201Y010952D01* +X038201Y010052D01* +X038101Y010052D01* +X038101Y009452D01* +X038101Y006752D01* +X035601Y006752D01* +X035601Y016752D01* +X038101Y016752D01* +X038101Y015052D01* +X038101Y014452D01* +X038101Y012552D01* +X038201Y012552D01* +X038601Y012552D01* +X038601Y014102D01* +X038401Y014252D01* +X038401Y014452D01* +X038401Y015052D01* +X038201Y015052D01* +X038101Y015052D01* +X038401Y015052D02* +X038401Y015252D01* +X038601Y015402D01* +X038601Y017252D01* +X035101Y017252D01* +X035101Y016752D01* +X035001Y016752D01* +X035001Y016252D01* +X035101Y016252D01* +X035101Y012002D01* +X035001Y012002D01* +X037801Y012552D02* +X037801Y010952D01* +X038101Y010952D01* +X038201Y010952D01* +X038101Y010952D02* +X038101Y010052D01* +X038201Y010052D02* +X038401Y010052D01* +X038401Y009452D02* +X038201Y009452D01* +X038101Y009452D01* +X038201Y009452D02* +X038201Y006652D01* +X038001Y004752D02* +X038701Y004752D01* +X038721Y004750D01* +X038739Y004744D01* +X038757Y004735D01* +X038772Y004723D01* +X038784Y004708D01* +X038793Y004690D01* +X038799Y004672D01* +X038801Y004652D01* +X038801Y004502D01* +X038751Y004452D01* +X038751Y002552D01* +X038801Y002502D01* +X038801Y002352D01* +X038799Y002332D01* +X038793Y002314D01* +X038784Y002296D01* +X038772Y002281D01* +X038757Y002269D01* +X038739Y002260D01* +X038721Y002254D01* +X038701Y002252D01* +X038001Y002252D01* +X037981Y002254D01* +X037963Y002260D01* +X037945Y002269D01* +X037930Y002281D01* +X037918Y002296D01* +X037909Y002314D01* +X037903Y002332D01* +X037901Y002352D01* +X037901Y002502D01* +X037951Y002552D01* +X037951Y004452D01* +X037901Y004502D01* +X037901Y004652D01* +X037903Y004672D01* +X037909Y004690D01* +X037918Y004708D01* +X037930Y004723D01* +X037945Y004735D01* +X037963Y004744D01* +X037981Y004750D01* +X038001Y004752D01* +X037981Y004750D01* +X037963Y004744D01* +X037945Y004735D01* +X037930Y004723D01* +X037918Y004708D01* +X037909Y004690D01* +X037903Y004672D01* +X037901Y004652D01* +X037801Y004652D02* +X037801Y004502D01* +X037751Y004452D01* +X037751Y002552D01* +X037801Y002502D01* +X037801Y002352D01* +X037799Y002332D01* +X037793Y002314D01* +X037784Y002296D01* +X037772Y002281D01* +X037757Y002269D01* +X037739Y002260D01* +X037721Y002254D01* +X037701Y002252D01* +X037001Y002252D01* +X036981Y002254D01* +X036963Y002260D01* +X036945Y002269D01* +X036930Y002281D01* +X036918Y002296D01* +X036909Y002314D01* +X036903Y002332D01* +X036901Y002352D01* +X036901Y002502D01* +X036951Y002552D01* +X036951Y004452D01* +X036901Y004502D01* +X036901Y004652D01* +X036903Y004672D01* +X036909Y004690D01* +X036918Y004708D01* +X036930Y004723D01* +X036945Y004735D01* +X036963Y004744D01* +X036981Y004750D01* +X037001Y004752D01* +X037701Y004752D01* +X037721Y004750D01* +X037739Y004744D01* +X037757Y004735D01* +X037772Y004723D01* +X037784Y004708D01* +X037793Y004690D01* +X037799Y004672D01* +X037801Y004652D01* +X037799Y004672D01* +X037793Y004690D01* +X037784Y004708D01* +X037772Y004723D01* +X037757Y004735D01* +X037739Y004744D01* +X037721Y004750D01* +X037701Y004752D01* +X037001Y004752D02* +X036981Y004750D01* +X036963Y004744D01* +X036945Y004735D01* +X036930Y004723D01* +X036918Y004708D01* +X036909Y004690D01* +X036903Y004672D01* +X036901Y004652D01* +X036801Y004652D02* +X036801Y004502D01* +X036751Y004452D01* +X036751Y002552D01* +X036801Y002502D01* +X036801Y002352D01* +X036799Y002332D01* +X036793Y002314D01* +X036784Y002296D01* +X036772Y002281D01* +X036757Y002269D01* +X036739Y002260D01* +X036721Y002254D01* +X036701Y002252D01* +X036001Y002252D01* +X035981Y002254D01* +X035963Y002260D01* +X035945Y002269D01* +X035930Y002281D01* +X035918Y002296D01* +X035909Y002314D01* +X035903Y002332D01* +X035901Y002352D01* +X035901Y002502D01* +X035951Y002552D01* +X035951Y004452D01* +X035901Y004502D01* +X035901Y004652D01* +X035903Y004672D01* +X035909Y004690D01* +X035918Y004708D01* +X035930Y004723D01* +X035945Y004735D01* +X035963Y004744D01* +X035981Y004750D01* +X036001Y004752D01* +X036701Y004752D01* +X036721Y004750D01* +X036739Y004744D01* +X036757Y004735D01* +X036772Y004723D01* +X036784Y004708D01* +X036793Y004690D01* +X036799Y004672D01* +X036801Y004652D01* +X035801Y004652D02* +X035801Y004502D01* +X035751Y004452D01* +X035751Y002552D01* +X035801Y002502D01* +X035801Y002352D01* +X035799Y002332D01* +X035793Y002314D01* +X035784Y002296D01* +X035772Y002281D01* +X035757Y002269D01* +X035739Y002260D01* +X035721Y002254D01* +X035701Y002252D01* +X035001Y002252D01* +X034981Y002254D01* +X034963Y002260D01* +X034945Y002269D01* +X034930Y002281D01* +X034918Y002296D01* +X034909Y002314D01* +X034903Y002332D01* +X034901Y002352D01* +X034901Y002502D01* +X034951Y002552D01* +X034951Y004452D01* +X034901Y004502D01* +X034901Y004652D01* +X034903Y004672D01* +X034909Y004690D01* +X034918Y004708D01* +X034930Y004723D01* +X034945Y004735D01* +X034963Y004744D01* +X034981Y004750D01* +X035001Y004752D01* +X035701Y004752D01* +X035721Y004750D01* +X035739Y004744D01* +X035757Y004735D01* +X035772Y004723D01* +X035784Y004708D01* +X035793Y004690D01* +X035799Y004672D01* +X035801Y004652D01* +X035799Y004672D01* +X035793Y004690D01* +X035784Y004708D01* +X035772Y004723D01* +X035757Y004735D01* +X035739Y004744D01* +X035721Y004750D01* +X035701Y004752D01* +X035001Y004752D02* +X034981Y004750D01* +X034963Y004744D01* +X034945Y004735D01* +X034930Y004723D01* +X034918Y004708D01* +X034909Y004690D01* +X034903Y004672D01* +X034901Y004652D01* +X034801Y004652D02* +X034801Y004502D01* +X034751Y004452D01* +X034751Y002552D01* +X034801Y002502D01* +X034801Y002352D01* +X034799Y002332D01* +X034793Y002314D01* +X034784Y002296D01* +X034772Y002281D01* +X034757Y002269D01* +X034739Y002260D01* +X034721Y002254D01* +X034701Y002252D01* +X034001Y002252D01* +X033981Y002254D01* +X033963Y002260D01* +X033945Y002269D01* +X033930Y002281D01* +X033918Y002296D01* +X033909Y002314D01* +X033903Y002332D01* +X033901Y002352D01* +X033901Y002502D01* +X033951Y002552D01* +X033951Y004452D01* +X033901Y004502D01* +X033901Y004652D01* +X033903Y004672D01* +X033909Y004690D01* +X033918Y004708D01* +X033930Y004723D01* +X033945Y004735D01* +X033963Y004744D01* +X033981Y004750D01* +X034001Y004752D01* +X034701Y004752D01* +X034721Y004750D01* +X034739Y004744D01* +X034757Y004735D01* +X034772Y004723D01* +X034784Y004708D01* +X034793Y004690D01* +X034799Y004672D01* +X034801Y004652D01* +X034799Y004672D01* +X034793Y004690D01* +X034784Y004708D01* +X034772Y004723D01* +X034757Y004735D01* +X034739Y004744D01* +X034721Y004750D01* +X034701Y004752D01* +X034001Y004752D02* +X033981Y004750D01* +X033963Y004744D01* +X033945Y004735D01* +X033930Y004723D01* +X033918Y004708D01* +X033909Y004690D01* +X033903Y004672D01* +X033901Y004652D01* +X032351Y004752D02* +X032351Y003252D01* +X032101Y003002D01* +X031601Y003002D01* +X031351Y003252D01* +X031351Y004752D01* +X031101Y005002D01* +X030601Y005002D01* +X030351Y004752D01* +X030351Y003252D01* +X030101Y003002D01* +X029601Y003002D01* +X029351Y003252D01* +X029351Y004752D01* +X029101Y005002D01* +X028601Y005002D01* +X028351Y004752D01* +X028351Y003252D01* +X028101Y003002D01* +X027601Y003002D01* +X027351Y003252D01* +X027351Y004752D01* +X027101Y005002D01* +X026601Y005002D01* +X026351Y004752D01* +X026351Y003252D01* +X026101Y003002D01* +X025601Y003002D01* +X025351Y003252D01* +X025351Y004752D01* +X025601Y005002D01* +X026101Y005002D01* +X026351Y004752D01* +X027351Y004752D02* +X027601Y005002D01* +X028101Y005002D01* +X028351Y004752D01* +X029351Y004752D02* +X029601Y005002D01* +X030101Y005002D01* +X030351Y004752D01* +X031351Y004752D02* +X031601Y005002D01* +X032101Y005002D01* +X032351Y004752D01* +X031351Y003252D02* +X031101Y003002D01* +X030601Y003002D01* +X030351Y003252D01* +X029351Y003252D02* +X029101Y003002D01* +X028601Y003002D01* +X028351Y003252D01* +X027351Y003252D02* +X027101Y003002D01* +X026601Y003002D01* +X026351Y003252D01* +X025351Y003252D02* +X025101Y003002D01* +X024601Y003002D01* +X024351Y003252D01* +X024351Y004752D01* +X024601Y005002D01* +X025101Y005002D01* +X025351Y004752D01* +X024351Y004752D02* +X024101Y005002D01* +X023601Y005002D01* +X023351Y004752D01* +X023351Y003252D01* +X023101Y003002D01* +X022601Y003002D01* +X022351Y003252D01* +X022101Y003002D01* +X021601Y003002D01* +X021351Y003252D01* +X021351Y004752D01* +X021601Y005002D01* +X022101Y005002D01* +X022351Y004752D01* +X022601Y005002D01* +X023101Y005002D01* +X023351Y004752D01* +X022351Y004752D02* +X022351Y003252D01* +X023351Y003252D02* +X023601Y003002D01* +X024101Y003002D01* +X024351Y003252D01* +X021351Y003252D02* +X021101Y003002D01* +X020601Y003002D01* +X020351Y003252D01* +X020351Y004752D01* +X020601Y005002D01* +X021101Y005002D01* +X021351Y004752D01* +X020351Y004752D02* +X020101Y005002D01* +X019601Y005002D01* +X019351Y004752D01* +X019351Y003252D01* +X019101Y003002D01* +X018601Y003002D01* +X018351Y003252D01* +X018351Y004752D01* +X018601Y005002D01* +X019101Y005002D01* +X019351Y004752D01* +X018351Y004752D02* +X018101Y005002D01* +X017601Y005002D01* +X017351Y004752D01* +X017351Y003252D01* +X017101Y003002D01* +X016601Y003002D01* +X016351Y003252D01* +X016351Y004752D01* +X016601Y005002D01* +X017101Y005002D01* +X017351Y004752D01* +X016351Y004752D02* +X016101Y005002D01* +X015601Y005002D01* +X015351Y004752D01* +X015351Y003252D01* +X015101Y003002D01* +X014601Y003002D01* +X014351Y003252D01* +X014351Y004752D01* +X014601Y005002D01* +X015101Y005002D01* +X015351Y004752D01* +X014351Y004752D02* +X014101Y005002D01* +X013601Y005002D01* +X013351Y004752D01* +X013101Y005002D01* +X012601Y005002D01* +X012351Y004752D01* +X012101Y005002D01* +X011601Y005002D01* +X011351Y004752D01* +X011101Y005002D01* +X010601Y005002D01* +X010351Y004752D01* +X010101Y005002D01* +X009601Y005002D01* +X009351Y004752D01* +X009101Y005002D01* +X008601Y005002D01* +X008351Y004752D01* +X008101Y005002D01* +X007601Y005002D01* +X007351Y004752D01* +X007351Y003252D01* +X007601Y003002D01* +X008101Y003002D01* +X008351Y003252D01* +X008601Y003002D01* +X009101Y003002D01* +X009351Y003252D01* +X009601Y003002D01* +X010101Y003002D01* +X010351Y003252D01* +X010601Y003002D01* +X011101Y003002D01* +X011351Y003252D01* +X011601Y003002D01* +X012101Y003002D01* +X012351Y003252D01* +X012351Y004752D01* +X013351Y004752D02* +X013351Y003252D01* +X013101Y003002D01* +X012601Y003002D01* +X012351Y003252D01* +X013351Y003252D02* +X013601Y003002D01* +X014101Y003002D01* +X014351Y003252D01* +X015351Y003252D02* +X015601Y003002D01* +X016101Y003002D01* +X016351Y003252D01* +X017351Y003252D02* +X017601Y003002D01* +X018101Y003002D01* +X018351Y003252D01* +X019351Y003252D02* +X019601Y003002D01* +X020101Y003002D01* +X020351Y003252D01* +X011351Y003252D02* +X011351Y004752D01* +X010351Y004752D02* +X010351Y003252D01* +X009351Y003252D02* +X009351Y004752D01* +X008351Y004752D02* +X008351Y003252D01* +X005101Y006352D02* +X005101Y006452D01* +X005001Y006452D01* +X005101Y006452D02* +X005101Y006552D01* +X005101Y006452D02* +X005201Y006452D01* +X004601Y007002D02* +X004605Y007093D01* +X004618Y007183D01* +X004638Y007272D01* +X004667Y007358D01* +X004703Y007442D01* +X004747Y007522D01* +X004797Y007597D01* +X004855Y007668D01* +X004918Y007733D01* +X004988Y007792D01* +X005062Y007844D01* +X005141Y007890D01* +X005224Y007928D01* +X005309Y007959D01* +X005398Y007981D01* +X005487Y007996D01* +X005578Y008002D01* +X005669Y008000D01* +X005760Y007989D01* +X005849Y007971D01* +X005936Y007944D01* +X006020Y007910D01* +X006101Y007868D01* +X006178Y007819D01* +X006250Y007763D01* +X006316Y007701D01* +X006377Y007633D01* +X006431Y007560D01* +X006478Y007482D01* +X006518Y007400D01* +X006551Y007315D01* +X006575Y007228D01* +X006592Y007138D01* +X006600Y007048D01* +X006600Y006956D01* +X006592Y006866D01* +X006575Y006776D01* +X006551Y006689D01* +X006518Y006604D01* +X006478Y006522D01* +X006431Y006444D01* +X006377Y006371D01* +X006316Y006303D01* +X006250Y006241D01* +X006178Y006185D01* +X006101Y006136D01* +X006020Y006094D01* +X005936Y006060D01* +X005849Y006033D01* +X005760Y006015D01* +X005669Y006004D01* +X005578Y006002D01* +X005487Y006008D01* +X005398Y006023D01* +X005309Y006045D01* +X005224Y006076D01* +X005141Y006114D01* +X005062Y006160D01* +X004988Y006212D01* +X004918Y006271D01* +X004855Y006336D01* +X004797Y006407D01* +X004747Y006482D01* +X004703Y006562D01* +X004667Y006646D01* +X004638Y006732D01* +X004618Y006821D01* +X004605Y006911D01* +X004601Y007002D01* +X000601Y007302D02* +X000601Y005702D01* +X000801Y017802D02* +X001001Y017802D01* +X001101Y017802D01* +X001101Y022202D02* +X001401Y022202D01* +X001401Y023802D01* +X001101Y023802D01* +X000601Y023802D02* +X000601Y022202D01* +X007351Y022752D02* +X007351Y021252D01* +X007601Y021002D01* +X008101Y021002D01* +X008351Y021252D01* +X008601Y021002D01* +X009101Y021002D01* +X009351Y021252D01* +X009601Y021002D01* +X010101Y021002D01* +X010351Y021252D01* +X010601Y021002D01* +X011101Y021002D01* +X011351Y021252D01* +X011601Y021002D01* +X012101Y021002D01* +X012351Y021252D01* +X012351Y022752D01* +X012101Y023002D01* +X011601Y023002D01* +X011351Y022752D01* +X011101Y023002D01* +X010601Y023002D01* +X010351Y022752D01* +X010101Y023002D01* +X009601Y023002D01* +X009351Y022752D01* +X009101Y023002D01* +X008601Y023002D01* +X008351Y022752D01* +X008101Y023002D01* +X007601Y023002D01* +X007351Y022752D01* +X008351Y022752D02* +X008351Y021252D01* +X009351Y021252D02* +X009351Y022752D01* +X010351Y022752D02* +X010351Y021252D01* +X011351Y021252D02* +X011351Y022752D01* +X012351Y022752D02* +X012601Y023002D01* +X013101Y023002D01* +X013351Y022752D01* +X013351Y021252D01* +X013101Y021002D01* +X012601Y021002D01* +X012351Y021252D01* +X013351Y021252D02* +X013601Y021002D01* +X014101Y021002D01* +X014351Y021252D01* +X014351Y022752D01* +X014601Y023002D01* +X015101Y023002D01* +X015351Y022752D01* +X015351Y021252D01* +X015101Y021002D01* +X014601Y021002D01* +X014351Y021252D01* +X015351Y021252D02* +X015601Y021002D01* +X016101Y021002D01* +X016351Y021252D01* +X016351Y022752D01* +X016601Y023002D01* +X017101Y023002D01* +X017351Y022752D01* +X017351Y021252D01* +X017101Y021002D01* +X016601Y021002D01* +X016351Y021252D01* +X017351Y021252D02* +X017601Y021002D01* +X018101Y021002D01* +X018351Y021252D01* +X018351Y022752D01* +X018601Y023002D01* +X019101Y023002D01* +X019351Y022752D01* +X019351Y021252D01* +X019101Y021002D01* +X018601Y021002D01* +X018351Y021252D01* +X019351Y021252D02* +X019601Y021002D01* +X020101Y021002D01* +X020351Y021252D01* +X020351Y022752D01* +X020601Y023002D01* +X021101Y023002D01* +X021351Y022752D01* +X021351Y021252D01* +X021101Y021002D01* +X020601Y021002D01* +X020351Y021252D01* +X021351Y021252D02* +X021601Y021002D01* +X022101Y021002D01* +X022351Y021252D01* +X022601Y021002D01* +X023101Y021002D01* +X023351Y021252D01* +X023351Y022752D01* +X023601Y023002D01* +X024101Y023002D01* +X024351Y022752D01* +X024351Y021252D01* +X024101Y021002D01* +X023601Y021002D01* +X023351Y021252D01* +X024351Y021252D02* +X024601Y021002D01* +X025101Y021002D01* +X025351Y021252D01* +X025351Y022752D01* +X025601Y023002D01* +X026101Y023002D01* +X026351Y022752D01* +X026351Y021252D01* +X026101Y021002D01* +X025601Y021002D01* +X025351Y021252D01* +X026351Y021252D02* +X026601Y021002D01* +X027101Y021002D01* +X027351Y021252D01* +X027351Y022752D01* +X027101Y023002D01* +X026601Y023002D01* +X026351Y022752D01* +X027351Y022752D02* +X027601Y023002D01* +X028101Y023002D01* +X028351Y022752D01* +X028351Y021252D01* +X028101Y021002D01* +X027601Y021002D01* +X027351Y021252D01* +X028351Y021252D02* +X028601Y021002D01* +X029101Y021002D01* +X029351Y021252D01* +X029351Y022752D01* +X029101Y023002D01* +X028601Y023002D01* +X028351Y022752D01* +X029351Y022752D02* +X029601Y023002D01* +X030101Y023002D01* +X030351Y022752D01* +X030351Y021252D01* +X030101Y021002D01* +X029601Y021002D01* +X029351Y021252D01* +X030351Y021252D02* +X030601Y021002D01* +X031101Y021002D01* +X031351Y021252D01* +X031351Y022752D01* +X031101Y023002D01* +X030601Y023002D01* +X030351Y022752D01* +X031351Y022752D02* +X031601Y023002D01* +X032101Y023002D01* +X032351Y022752D01* +X032351Y021252D01* +X032101Y021002D01* +X031601Y021002D01* +X031351Y021252D01* +X030751Y023802D02* +X029951Y023802D01* +X029931Y023804D01* +X029913Y023810D01* +X029895Y023819D01* +X029880Y023831D01* +X029868Y023846D01* +X029859Y023864D01* +X029853Y023882D01* +X029851Y023902D01* +X029851Y026602D01* +X029853Y026622D01* +X029859Y026640D01* +X029868Y026658D01* +X029880Y026673D01* +X029895Y026685D01* +X029913Y026694D01* +X029931Y026700D01* +X029951Y026702D01* +X030751Y026702D01* +X030771Y026700D01* +X030789Y026694D01* +X030807Y026685D01* +X030822Y026673D01* +X030834Y026658D01* +X030843Y026640D01* +X030849Y026622D01* +X030851Y026602D01* +X030851Y023902D01* +X030849Y023882D01* +X030843Y023864D01* +X030834Y023846D01* +X030822Y023831D01* +X030807Y023819D01* +X030789Y023810D01* +X030771Y023804D01* +X030751Y023802D01* +X030351Y024702D02* +X030351Y025132D01* +X030351Y025382D02* +X030351Y025802D01* +X026201Y028702D02* +X025701Y028702D01* +X025951Y028452D02* +X025951Y028952D01* +X023001Y028102D02* +X023001Y030902D01* +X036651Y030902D01* +X038751Y030902D01* +X038751Y028102D01* +X036651Y028102D01* +X036651Y030902D01* +X035451Y028702D02* +X034951Y028702D01* +X036651Y028102D02* +X023001Y028102D01* +X022101Y028502D02* +X021851Y028252D01* +X021351Y028252D01* +X021101Y028502D01* +X021101Y029002D01* +X021351Y029252D01* +X021101Y029502D01* +X021101Y030002D01* +X021351Y030252D01* +X021851Y030252D01* +X022101Y030002D01* +X022101Y029502D01* +X021851Y029252D01* +X022101Y029002D01* +X022101Y028502D01* +X021851Y029252D02* +X021351Y029252D01* +X019351Y030602D02* +X019301Y030652D01* +X019201Y030752D01* +X018751Y030752D01* +X018651Y030652D01* +X019301Y030652D01* +X019351Y030602D02* +X019351Y024402D01* +X018601Y024402D01* +X018601Y024602D01* +X018601Y030502D01* +X018601Y030602D01* +X018251Y030602D01* +X018251Y030502D01* +X018601Y030502D01* +X018601Y030602D02* +X018651Y030652D01* +X018251Y030602D02* +X018201Y030652D01* +X007401Y030652D01* +X007501Y030752D01* +X018101Y030752D01* +X018201Y030652D01* +X018251Y030502D02* +X018251Y024602D01* +X018601Y024602D01* +X018601Y024402D02* +X018251Y024402D01* +X007351Y024402D01* +X007501Y024252D01* +X018101Y024252D01* +X018251Y024402D01* +X018251Y024602D01* +X018601Y024402D02* +X018751Y024252D01* +X019201Y024252D01* +X019351Y024402D01* +X019601Y023002D02* +X020101Y023002D01* +X020351Y022752D01* +X019601Y023002D02* +X019351Y022752D01* +X018351Y022752D02* +X018101Y023002D01* +X017601Y023002D01* +X017351Y022752D01* +X016351Y022752D02* +X016101Y023002D01* +X015601Y023002D01* +X015351Y022752D01* +X014351Y022752D02* +X014101Y023002D01* +X013601Y023002D01* +X013351Y022752D01* +X017201Y026902D02* +X017401Y026902D01* +X017401Y027502D01* +X017851Y027502D01* +X017701Y027302D02* +X017701Y027002D01* +X017851Y027152D02* +X017551Y027152D01* +X017401Y027502D02* +X017401Y028102D01* +X017201Y028102D01* +X017201Y026902D01* +X016851Y027502D02* +X007801Y027502D01* +X007351Y024402D02* +X007351Y030602D01* +X007401Y030652D01* +X001101Y027802D02* +X001001Y027802D01* +X000801Y027802D01* +X000801Y027202D02* +X001001Y027202D01* +X001101Y027202D01* +X021351Y022752D02* +X021601Y023002D01* +X022101Y023002D01* +X022351Y022752D01* +X022601Y023002D01* +X023101Y023002D01* +X023351Y022752D01* +X024351Y022752D02* +X024601Y023002D01* +X025101Y023002D01* +X025351Y022752D01* +X022351Y022752D02* +X022351Y021252D01* +X030851Y018002D02* +X030855Y018093D01* +X030868Y018183D01* +X030888Y018272D01* +X030917Y018358D01* +X030953Y018442D01* +X030997Y018522D01* +X031047Y018597D01* +X031105Y018668D01* +X031168Y018733D01* +X031238Y018792D01* +X031312Y018844D01* +X031391Y018890D01* +X031474Y018928D01* +X031559Y018959D01* +X031648Y018981D01* +X031737Y018996D01* +X031828Y019002D01* +X031919Y019000D01* +X032010Y018989D01* +X032099Y018971D01* +X032186Y018944D01* +X032270Y018910D01* +X032351Y018868D01* +X032428Y018819D01* +X032500Y018763D01* +X032566Y018701D01* +X032627Y018633D01* +X032681Y018560D01* +X032728Y018482D01* +X032768Y018400D01* +X032801Y018315D01* +X032825Y018228D01* +X032842Y018138D01* +X032850Y018048D01* +X032850Y017956D01* +X032842Y017866D01* +X032825Y017776D01* +X032801Y017689D01* +X032768Y017604D01* +X032728Y017522D01* +X032681Y017444D01* +X032627Y017371D01* +X032566Y017303D01* +X032500Y017241D01* +X032428Y017185D01* +X032351Y017136D01* +X032270Y017094D01* +X032186Y017060D01* +X032099Y017033D01* +X032010Y017015D01* +X031919Y017004D01* +X031828Y017002D01* +X031737Y017008D01* +X031648Y017023D01* +X031559Y017045D01* +X031474Y017076D01* +X031391Y017114D01* +X031312Y017160D01* +X031238Y017212D01* +X031168Y017271D01* +X031105Y017336D01* +X031047Y017407D01* +X030997Y017482D01* +X030953Y017562D01* +X030917Y017646D01* +X030888Y017732D01* +X030868Y017821D01* +X030855Y017911D01* +X030851Y018002D01* +X032251Y018552D02* +X032351Y018552D01* +X032451Y018552D01* +X032351Y018552D02* +X032351Y018452D01* +X032351Y018552D02* +X032351Y018652D01* +X033351Y015902D02* +X033351Y015802D01* +X033451Y015802D01* +X033351Y015802D02* +X033351Y015702D01* +X033351Y015802D02* +X033251Y015802D01* +X031851Y015252D02* +X031855Y015343D01* +X031868Y015433D01* +X031888Y015522D01* +X031917Y015608D01* +X031953Y015692D01* +X031997Y015772D01* +X032047Y015847D01* +X032105Y015918D01* +X032168Y015983D01* +X032238Y016042D01* +X032312Y016094D01* +X032391Y016140D01* +X032474Y016178D01* +X032559Y016209D01* +X032648Y016231D01* +X032737Y016246D01* +X032828Y016252D01* +X032919Y016250D01* +X033010Y016239D01* +X033099Y016221D01* +X033186Y016194D01* +X033270Y016160D01* +X033351Y016118D01* +X033428Y016069D01* +X033500Y016013D01* +X033566Y015951D01* +X033627Y015883D01* +X033681Y015810D01* +X033728Y015732D01* +X033768Y015650D01* +X033801Y015565D01* +X033825Y015478D01* +X033842Y015388D01* +X033850Y015298D01* +X033850Y015206D01* +X033842Y015116D01* +X033825Y015026D01* +X033801Y014939D01* +X033768Y014854D01* +X033728Y014772D01* +X033681Y014694D01* +X033627Y014621D01* +X033566Y014553D01* +X033500Y014491D01* +X033428Y014435D01* +X033351Y014386D01* +X033270Y014344D01* +X033186Y014310D01* +X033099Y014283D01* +X033010Y014265D01* +X032919Y014254D01* +X032828Y014252D01* +X032737Y014258D01* +X032648Y014273D01* +X032559Y014295D01* +X032474Y014326D01* +X032391Y014364D01* +X032312Y014410D01* +X032238Y014462D01* +X032168Y014521D01* +X032105Y014586D01* +X032047Y014657D01* +X031997Y014732D01* +X031953Y014812D01* +X031917Y014896D01* +X031888Y014982D01* +X031868Y015071D01* +X031855Y015161D01* +X031851Y015252D01* +X037801Y012552D02* +X038101Y012552D01* +X038601Y012552D02* +X038601Y010952D01* +X038401Y014452D02* +X038201Y014452D01* +X038101Y014452D01* +X039101Y018452D02* +X044001Y018452D01* +X044001Y018802D01* +X044601Y018802D01* +X044601Y018452D01* +X045351Y018452D01* +X045351Y022552D01* +X044601Y022552D01* +X044601Y022202D01* +X044001Y022202D01* +X044001Y022552D01* +X039101Y022552D01* +X039101Y018452D01* +X043291Y020502D02* +X043294Y020567D01* +X043303Y020631D01* +X043317Y020694D01* +X043338Y020755D01* +X043363Y020814D01* +X043394Y020871D01* +X043430Y020925D01* +X043471Y020975D01* +X043516Y021021D01* +X043566Y021063D01* +X043618Y021100D01* +X043674Y021132D01* +X043733Y021159D01* +X043794Y021181D01* +X043857Y021197D01* +X043920Y021207D01* +X043985Y021212D01* +X044049Y021210D01* +X044114Y021203D01* +X044177Y021190D01* +X044239Y021171D01* +X044299Y021147D01* +X044356Y021117D01* +X044410Y021082D01* +X044461Y021042D01* +X044509Y020998D01* +X044552Y020950D01* +X044590Y020898D01* +X044624Y020843D01* +X044652Y020785D01* +X044675Y020724D01* +X044693Y020662D01* +X044704Y020599D01* +X044710Y020534D01* +X044710Y020470D01* +X044704Y020405D01* +X044693Y020342D01* +X044675Y020280D01* +X044652Y020219D01* +X044624Y020161D01* +X044590Y020106D01* +X044552Y020054D01* +X044509Y020006D01* +X044461Y019962D01* +X044410Y019922D01* +X044356Y019887D01* +X044299Y019857D01* +X044239Y019833D01* +X044177Y019814D01* +X044114Y019801D01* +X044049Y019794D01* +X043985Y019792D01* +X043920Y019797D01* +X043857Y019807D01* +X043794Y019823D01* +X043733Y019845D01* +X043674Y019872D01* +X043618Y019904D01* +X043566Y019941D01* +X043516Y019983D01* +X043471Y020029D01* +X043430Y020079D01* +X043394Y020133D01* +X043363Y020190D01* +X043338Y020249D01* +X043317Y020310D01* +X043303Y020373D01* +X043294Y020437D01* +X043291Y020502D01* +X044001Y022952D02* +X044001Y023302D01* +X044601Y023302D01* +X044601Y022952D01* +X045351Y022952D01* +X045351Y027052D01* +X044601Y027052D01* +X044601Y026702D01* +X044001Y026702D01* +X044001Y027052D01* +X039101Y027052D01* +X039101Y022952D01* +X044001Y022952D01* +X043291Y025002D02* +X043294Y025067D01* +X043303Y025131D01* +X043317Y025194D01* +X043338Y025255D01* +X043363Y025314D01* +X043394Y025371D01* +X043430Y025425D01* +X043471Y025475D01* +X043516Y025521D01* +X043566Y025563D01* +X043618Y025600D01* +X043674Y025632D01* +X043733Y025659D01* +X043794Y025681D01* +X043857Y025697D01* +X043920Y025707D01* +X043985Y025712D01* +X044049Y025710D01* +X044114Y025703D01* +X044177Y025690D01* +X044239Y025671D01* +X044299Y025647D01* +X044356Y025617D01* +X044410Y025582D01* +X044461Y025542D01* +X044509Y025498D01* +X044552Y025450D01* +X044590Y025398D01* +X044624Y025343D01* +X044652Y025285D01* +X044675Y025224D01* +X044693Y025162D01* +X044704Y025099D01* +X044710Y025034D01* +X044710Y024970D01* +X044704Y024905D01* +X044693Y024842D01* +X044675Y024780D01* +X044652Y024719D01* +X044624Y024661D01* +X044590Y024606D01* +X044552Y024554D01* +X044509Y024506D01* +X044461Y024462D01* +X044410Y024422D01* +X044356Y024387D01* +X044299Y024357D01* +X044239Y024333D01* +X044177Y024314D01* +X044114Y024301D01* +X044049Y024294D01* +X043985Y024292D01* +X043920Y024297D01* +X043857Y024307D01* +X043794Y024323D01* +X043733Y024345D01* +X043674Y024372D01* +X043618Y024404D01* +X043566Y024441D01* +X043516Y024483D01* +X043471Y024529D01* +X043430Y024579D01* +X043394Y024633D01* +X043363Y024690D01* +X043338Y024749D01* +X043317Y024810D01* +X043303Y024873D01* +X043294Y024937D01* +X043291Y025002D01* +X038701Y004752D02* +X038721Y004750D01* +X038739Y004744D01* +X038757Y004735D01* +X038772Y004723D01* +X038784Y004708D01* +X038793Y004690D01* +X038799Y004672D01* +X038801Y004652D01* +X038801Y002352D02* +X038799Y002332D01* +X038793Y002314D01* +X038784Y002296D01* +X038772Y002281D01* +X038757Y002269D01* +X038739Y002260D01* +X038721Y002254D01* +X038701Y002252D01* +X038001Y002252D02* +X037981Y002254D01* +X037963Y002260D01* +X037945Y002269D01* +X037930Y002281D01* +X037918Y002296D01* +X037909Y002314D01* +X037903Y002332D01* +X037901Y002352D01* +X037801Y002352D02* +X037799Y002332D01* +X037793Y002314D01* +X037784Y002296D01* +X037772Y002281D01* +X037757Y002269D01* +X037739Y002260D01* +X037721Y002254D01* +X037701Y002252D01* +X037001Y002252D02* +X036981Y002254D01* +X036963Y002260D01* +X036945Y002269D01* +X036930Y002281D01* +X036918Y002296D01* +X036909Y002314D01* +X036903Y002332D01* +X036901Y002352D01* +X035801Y002352D02* +X035799Y002332D01* +X035793Y002314D01* +X035784Y002296D01* +X035772Y002281D01* +X035757Y002269D01* +X035739Y002260D01* +X035721Y002254D01* +X035701Y002252D01* +X035001Y002252D02* +X034981Y002254D01* +X034963Y002260D01* +X034945Y002269D01* +X034930Y002281D01* +X034918Y002296D01* +X034909Y002314D01* +X034903Y002332D01* +X034901Y002352D01* +X034801Y002352D02* +X034799Y002332D01* +X034793Y002314D01* +X034784Y002296D01* +X034772Y002281D01* +X034757Y002269D01* +X034739Y002260D01* +X034721Y002254D01* +X034701Y002252D01* +X034001Y002252D02* +X033981Y002254D01* +X033963Y002260D01* +X033945Y002269D01* +X033930Y002281D01* +X033918Y002296D01* +X033909Y002314D01* +X033903Y002332D01* +X033901Y002352D01* +D11* +X030691Y006412D02* +X030691Y006706D01* +X030691Y006559D02* +X030250Y006559D01* +X030397Y006412D01* +X029441Y006422D02* +X029441Y006716D01* +X029441Y006569D02* +X029000Y006569D01* +X029147Y006422D01* +X029221Y006882D02* +X029221Y007102D01* +X029294Y007176D01* +X029367Y007176D01* +X029441Y007102D01* +X029441Y006956D01* +X029367Y006882D01* +X029221Y006882D01* +X029074Y007029D01* +X029000Y007176D01* +X026691Y006706D02* +X026691Y006412D01* +X026691Y006559D02* +X026250Y006559D01* +X026397Y006412D01* +X025441Y006422D02* +X025441Y006716D01* +X025441Y006569D02* +X025000Y006569D01* +X025147Y006422D01* +X025221Y006882D02* +X025221Y007102D01* +X025294Y007176D01* +X025367Y007176D01* +X025441Y007102D01* +X025441Y006956D01* +X025367Y006882D01* +X025221Y006882D01* +X025074Y007029D01* +X025000Y007176D01* +X022691Y006706D02* +X022691Y006412D01* +X022691Y006559D02* +X022250Y006559D01* +X022397Y006412D01* +X021441Y006422D02* +X021441Y006716D01* +X021441Y006569D02* +X021000Y006569D01* +X021147Y006422D01* +X021221Y006882D02* +X021221Y007102D01* +X021294Y007176D01* +X021367Y007176D01* +X021441Y007102D01* +X021441Y006956D01* +X021367Y006882D01* +X021221Y006882D01* +X021074Y007029D01* +X021000Y007176D01* +X018691Y006706D02* +X018691Y006412D01* +X018691Y006559D02* +X018250Y006559D01* +X018397Y006412D01* +X017441Y006422D02* +X017441Y006716D01* +X017441Y006569D02* +X017000Y006569D01* +X017147Y006422D01* +X017221Y006882D02* +X017221Y007102D01* +X017294Y007176D01* +X017367Y007176D01* +X017441Y007102D01* +X017441Y006956D01* +X017367Y006882D01* +X017221Y006882D01* +X017074Y007029D01* +X017000Y007176D01* +X014691Y006706D02* +X014691Y006412D01* +X014691Y006559D02* +X014250Y006559D01* +X014397Y006412D01* +X013441Y006422D02* +X013441Y006716D01* +X013441Y006569D02* +X013000Y006569D01* +X013147Y006422D01* +X013221Y006882D02* +X013221Y007102D01* +X013294Y007176D01* +X013367Y007176D01* +X013441Y007102D01* +X013441Y006956D01* +X013367Y006882D01* +X013221Y006882D01* +X013074Y007029D01* +X013000Y007176D01* +X010691Y006706D02* +X010691Y006412D01* +X010691Y006559D02* +X010250Y006559D01* +X010397Y006412D01* +X009441Y006422D02* +X009441Y006716D01* +X009441Y006569D02* +X009000Y006569D01* +X009147Y006422D01* +X009221Y006882D02* +X009221Y007102D01* +X009294Y007176D01* +X009367Y007176D01* +X009441Y007102D01* +X009441Y006956D01* +X009367Y006882D01* +X009221Y006882D01* +X009074Y007029D01* +X009000Y007176D01* +X003071Y010572D02* +X002777Y010865D01* +X002704Y010865D01* +X002630Y010792D01* +X002630Y010645D01* +X002704Y010572D01* +X003071Y010572D02* +X003071Y010865D01* +X002071Y010865D02* +X002071Y010572D01* +X002071Y010718D02* +X001630Y010718D01* +X001777Y010572D01* +X001147Y006765D02* +X001074Y006765D01* +X001001Y006692D01* +X001001Y006472D01* +X001147Y006472D01* +X001221Y006545D01* +X001221Y006692D01* +X001147Y006765D01* +X000854Y006618D02* +X001001Y006472D01* +X000854Y006618D02* +X000780Y006765D01* +X000780Y006158D02* +X001221Y006158D01* +X001221Y006011D02* +X001221Y006305D01* +X000927Y006011D02* +X000780Y006158D01* +X009000Y013355D02* +X009074Y013282D01* +X009147Y013282D01* +X009221Y013355D01* +X009221Y013576D01* +X009367Y013576D02* +X009074Y013576D01* +X009000Y013502D01* +X009000Y013355D01* +X009367Y013282D02* +X009441Y013355D01* +X009441Y013502D01* +X009367Y013576D01* +X010250Y013502D02* +X010250Y013355D01* +X010324Y013282D01* +X010397Y013282D01* +X010471Y013355D01* +X010471Y013502D01* +X010544Y013576D01* +X010617Y013576D01* +X010691Y013502D01* +X010691Y013355D01* +X010617Y013282D01* +X010544Y013282D01* +X010471Y013355D01* +X010471Y013502D02* +X010397Y013576D01* +X010324Y013576D01* +X010250Y013502D01* +X013000Y013502D02* +X013000Y013355D01* +X013074Y013282D01* +X013147Y013282D01* +X013221Y013355D01* +X013221Y013576D01* +X013367Y013576D02* +X013074Y013576D01* +X013000Y013502D01* +X013367Y013576D02* +X013441Y013502D01* +X013441Y013355D01* +X013367Y013282D01* +X014250Y013355D02* +X014250Y013502D01* +X014324Y013576D01* +X014397Y013576D01* +X014471Y013502D01* +X014471Y013355D01* +X014397Y013282D01* +X014324Y013282D01* +X014250Y013355D01* +X014471Y013355D02* +X014544Y013282D01* +X014617Y013282D01* +X014691Y013355D01* +X014691Y013502D01* +X014617Y013576D01* +X014544Y013576D01* +X014471Y013502D01* +X017000Y013502D02* +X017000Y013355D01* +X017074Y013282D01* +X017147Y013282D01* +X017221Y013355D01* +X017221Y013576D01* +X017367Y013576D02* +X017074Y013576D01* +X017000Y013502D01* +X017367Y013576D02* +X017441Y013502D01* +X017441Y013355D01* +X017367Y013282D01* +X018250Y013355D02* +X018250Y013502D01* +X018324Y013576D01* +X018397Y013576D01* +X018471Y013502D01* +X018471Y013355D01* +X018397Y013282D01* +X018324Y013282D01* +X018250Y013355D01* +X018471Y013355D02* +X018544Y013282D01* +X018617Y013282D01* +X018691Y013355D01* +X018691Y013502D01* +X018617Y013576D01* +X018544Y013576D01* +X018471Y013502D01* +X021000Y013502D02* +X021000Y013355D01* +X021074Y013282D01* +X021147Y013282D01* +X021221Y013355D01* +X021221Y013576D01* +X021367Y013576D02* +X021074Y013576D01* +X021000Y013502D01* +X021367Y013576D02* +X021441Y013502D01* +X021441Y013355D01* +X021367Y013282D01* +X022250Y013355D02* +X022250Y013502D01* +X022324Y013576D01* +X022397Y013576D01* +X022471Y013502D01* +X022471Y013355D01* +X022397Y013282D01* +X022324Y013282D01* +X022250Y013355D01* +X022471Y013355D02* +X022544Y013282D01* +X022617Y013282D01* +X022691Y013355D01* +X022691Y013502D01* +X022617Y013576D01* +X022544Y013576D01* +X022471Y013502D01* +X025000Y013502D02* +X025000Y013355D01* +X025074Y013282D01* +X025147Y013282D01* +X025221Y013355D01* +X025221Y013576D01* +X025367Y013576D02* +X025074Y013576D01* +X025000Y013502D01* +X025367Y013576D02* +X025441Y013502D01* +X025441Y013355D01* +X025367Y013282D01* +X026250Y013355D02* +X026250Y013502D01* +X026324Y013576D01* +X026397Y013576D01* +X026471Y013502D01* +X026471Y013355D01* +X026397Y013282D01* +X026324Y013282D01* +X026250Y013355D01* +X026471Y013355D02* +X026544Y013282D01* +X026617Y013282D01* +X026691Y013355D01* +X026691Y013502D01* +X026617Y013576D01* +X026544Y013576D01* +X026471Y013502D01* +X029000Y013502D02* +X029000Y013355D01* +X029074Y013282D01* +X029147Y013282D01* +X029221Y013355D01* +X029221Y013576D01* +X029367Y013576D02* +X029074Y013576D01* +X029000Y013502D01* +X029367Y013576D02* +X029441Y013502D01* +X029441Y013355D01* +X029367Y013282D01* +X030250Y013355D02* +X030250Y013502D01* +X030324Y013576D01* +X030397Y013576D01* +X030471Y013502D01* +X030471Y013355D01* +X030397Y013282D01* +X030324Y013282D01* +X030250Y013355D01* +X030471Y013355D02* +X030544Y013282D01* +X030617Y013282D01* +X030691Y013355D01* +X030691Y013502D01* +X030617Y013576D01* +X030544Y013576D01* +X030471Y013502D01* +X029751Y016152D02* +X029604Y016152D01* +X029530Y016225D01* +X029530Y016372D01* +X029751Y016372D01* +X029824Y016299D01* +X029824Y016225D01* +X029751Y016152D01* +X029530Y016372D02* +X029677Y016519D01* +X029824Y016592D01* +X029217Y016592D02* +X029217Y016152D01* +X029070Y016152D02* +X029364Y016152D01* +X029070Y016446D02* +X029217Y016592D01* +X029540Y017402D02* +X029834Y017402D01* +X029687Y017402D02* +X029687Y017842D01* +X029540Y017696D01* +X022964Y017696D02* +X022891Y017622D01* +X022744Y017622D01* +X022670Y017696D01* +X022670Y017769D01* +X022744Y017842D01* +X022891Y017842D01* +X022964Y017769D01* +X022964Y017696D01* +X022891Y017622D02* +X022964Y017549D01* +X022964Y017475D01* +X022891Y017402D01* +X022744Y017402D01* +X022670Y017475D01* +X022670Y017549D01* +X022744Y017622D01* +X022744Y016592D02* +X022670Y016519D01* +X022670Y016446D01* +X022744Y016372D01* +X022964Y016372D01* +X022964Y016225D02* +X022964Y016519D01* +X022891Y016592D01* +X022744Y016592D01* +X022670Y016225D02* +X022744Y016152D01* +X022891Y016152D01* +X022964Y016225D01* +X021324Y016225D02* +X021324Y016299D01* +X021251Y016372D01* +X021030Y016372D01* +X021030Y016225D01* +X021104Y016152D01* +X021251Y016152D01* +X021324Y016225D01* +X021177Y016519D02* +X021030Y016372D01* +X021177Y016519D02* +X021324Y016592D01* +X020717Y016592D02* +X020570Y016446D01* +X020717Y016592D02* +X020717Y016152D01* +X020570Y016152D02* +X020864Y016152D01* +X021040Y017402D02* +X021334Y017402D01* +X021187Y017402D02* +X021187Y017842D01* +X021040Y017696D01* +X014464Y017696D02* +X014391Y017622D01* +X014244Y017622D01* +X014170Y017696D01* +X014170Y017769D01* +X014244Y017842D01* +X014391Y017842D01* +X014464Y017769D01* +X014464Y017696D01* +X014391Y017622D02* +X014464Y017549D01* +X014464Y017475D01* +X014391Y017402D01* +X014244Y017402D01* +X014170Y017475D01* +X014170Y017549D01* +X014244Y017622D01* +X014244Y016592D02* +X014170Y016519D01* +X014170Y016446D01* +X014244Y016372D01* +X014464Y016372D01* +X014464Y016225D02* +X014464Y016519D01* +X014391Y016592D01* +X014244Y016592D01* +X014170Y016225D02* +X014244Y016152D01* +X014391Y016152D01* +X014464Y016225D01* +X012824Y016225D02* +X012824Y016299D01* +X012751Y016372D01* +X012530Y016372D01* +X012530Y016225D01* +X012604Y016152D01* +X012751Y016152D01* +X012824Y016225D01* +X012677Y016519D02* +X012530Y016372D01* +X012677Y016519D02* +X012824Y016592D01* +X012217Y016592D02* +X012217Y016152D01* +X012070Y016152D02* +X012364Y016152D01* +X012070Y016446D02* +X012217Y016592D01* +X012540Y017402D02* +X012834Y017402D01* +X012687Y017402D02* +X012687Y017842D01* +X012540Y017696D01* +X005964Y017696D02* +X005891Y017622D01* +X005744Y017622D01* +X005670Y017696D01* +X005670Y017769D01* +X005744Y017842D01* +X005891Y017842D01* +X005964Y017769D01* +X005964Y017696D01* +X005891Y017622D02* +X005964Y017549D01* +X005964Y017475D01* +X005891Y017402D01* +X005744Y017402D01* +X005670Y017475D01* +X005670Y017549D01* +X005744Y017622D01* +X005744Y016592D02* +X005670Y016519D01* +X005670Y016446D01* +X005744Y016372D01* +X005964Y016372D01* +X005964Y016225D02* +X005964Y016519D01* +X005891Y016592D01* +X005744Y016592D01* +X005670Y016225D02* +X005744Y016152D01* +X005891Y016152D01* +X005964Y016225D01* +X001221Y022511D02* +X000927Y022805D01* +X000854Y022805D01* +X000780Y022731D01* +X000780Y022585D01* +X000854Y022511D01* +X001221Y022511D02* +X001221Y022805D01* +X001147Y022972D02* +X001221Y023045D01* +X001221Y023192D01* +X001147Y023265D01* +X001074Y023265D01* +X001001Y023192D01* +X001001Y022972D01* +X001147Y022972D01* +X001001Y022972D02* +X000854Y023118D01* +X000780Y023265D01* +X001777Y029572D02* +X001630Y029718D01* +X002071Y029718D01* +X002071Y029572D02* +X002071Y029865D01* +X002630Y029792D02* +X002630Y029645D01* +X002704Y029572D01* +X002630Y029792D02* +X002704Y029865D01* +X002777Y029865D01* +X003071Y029572D01* +X003071Y029865D01* +X007579Y030032D02* +X007872Y030032D01* +X007726Y030032D02* +X007726Y030472D01* +X007579Y030326D01* +X008039Y030252D02* +X008259Y030252D01* +X008333Y030179D01* +X008333Y030105D01* +X008259Y030032D01* +X008113Y030032D01* +X008039Y030105D01* +X008039Y030252D01* +X008186Y030399D01* +X008333Y030472D01* +X008500Y030326D02* +X008793Y030032D01* +X008960Y030105D02* +X009033Y030032D01* +X009180Y030032D01* +X009254Y030105D01* +X009254Y030179D01* +X009180Y030252D01* +X009107Y030252D01* +X009180Y030252D02* +X009254Y030326D01* +X009254Y030399D01* +X009180Y030472D01* +X009033Y030472D01* +X008960Y030399D01* +X008793Y030326D02* +X008500Y030032D01* +X009420Y030105D02* +X009714Y030399D01* +X009714Y030105D01* +X009640Y030032D01* +X009494Y030032D01* +X009420Y030105D01* +X009420Y030399D01* +X009494Y030472D01* +X009640Y030472D01* +X009714Y030399D01* +X037980Y012136D02* +X038054Y011989D01* +X038201Y011842D01* +X038201Y012062D01* +X038274Y012136D01* +X038347Y012136D01* +X038421Y012062D01* +X038421Y011916D01* +X038347Y011842D01* +X038201Y011842D01* +X038421Y011676D02* +X038421Y011382D01* +X038421Y011529D02* +X037980Y011529D01* +X038127Y011382D01* +X037571Y007576D02* +X037571Y007282D01* +X037571Y007429D02* +X037130Y007429D01* +X037277Y007282D01* +X036571Y007282D02* +X036571Y007576D01* +X036571Y007282D02* +X036277Y007576D01* +X036204Y007576D01* +X036130Y007502D01* +X036130Y007355D01* +X036204Y007282D01* +D12* +X034566Y007480D02* +X034477Y007392D01* +X034566Y007480D02* +X034566Y007657D01* +X034477Y007745D01* +X034389Y007745D01* +X034301Y007657D01* +X034301Y007569D01* +X034301Y007657D02* +X034212Y007745D01* +X034124Y007745D01* +X034035Y007657D01* +X034035Y007480D01* +X034124Y007392D01* +X034035Y007193D02* +X034035Y006839D01* +X034035Y007016D02* +X034566Y007016D01* +X034477Y006641D02* +X034566Y006552D01* +X034566Y006375D01* +X034477Y006287D01* +X034301Y006375D02* +X034212Y006287D01* +X034124Y006287D01* +X034035Y006375D01* +X034035Y006552D01* +X034124Y006641D01* +X034301Y006552D02* +X034389Y006641D01* +X034477Y006641D01* +X034301Y006552D02* +X034301Y006375D01* +X032977Y004745D02* +X032801Y004745D01* +X032712Y004657D01* +X032712Y004569D01* +X032801Y004392D01* +X032535Y004392D01* +X032535Y004745D01* +X032977Y004745D02* +X033066Y004657D01* +X033066Y004480D01* +X032977Y004392D01* +X032977Y004193D02* +X032535Y004193D01* +X032535Y003839D02* +X032977Y003839D01* +X033066Y003928D01* +X033066Y004105D01* +X032977Y004193D01* +X032977Y003641D02* +X033066Y003552D01* +X033066Y003287D01* +X032535Y003287D01* +X032535Y003552D01* +X032624Y003641D01* +X032712Y003641D01* +X032801Y003552D01* +X032801Y003287D01* +X032801Y003552D02* +X032889Y003641D01* +X032977Y003641D01* +X030687Y005327D02* +X030333Y005327D01* +X030510Y005327D02* +X030510Y005857D01* +X030333Y005681D01* +X030134Y005769D02* +X030046Y005857D01* +X029869Y005857D01* +X029781Y005769D01* +X029781Y005415D01* +X029869Y005327D01* +X030046Y005327D01* +X030134Y005415D01* +X029589Y005327D02* +X029413Y005327D01* +X029501Y005327D02* +X029501Y005857D01* +X029413Y005857D02* +X029589Y005857D01* +X026687Y005769D02* +X026687Y005681D01* +X026333Y005327D01* +X026687Y005327D01* +X026134Y005415D02* +X026046Y005327D01* +X025869Y005327D01* +X025781Y005415D01* +X025781Y005769D01* +X025869Y005857D01* +X026046Y005857D01* +X026134Y005769D01* +X026333Y005769D02* +X026422Y005857D01* +X026599Y005857D01* +X026687Y005769D01* +X025589Y005857D02* +X025413Y005857D01* +X025501Y005857D02* +X025501Y005327D01* +X025413Y005327D02* +X025589Y005327D01* +X022687Y005415D02* +X022599Y005327D01* +X022422Y005327D01* +X022333Y005415D01* +X022134Y005415D02* +X022046Y005327D01* +X021869Y005327D01* +X021781Y005415D01* +X021781Y005769D01* +X021869Y005857D01* +X022046Y005857D01* +X022134Y005769D01* +X022333Y005769D02* +X022422Y005857D01* +X022599Y005857D01* +X022687Y005769D01* +X022687Y005681D01* +X022599Y005592D01* +X022687Y005504D01* +X022687Y005415D01* +X022599Y005592D02* +X022510Y005592D01* +X021589Y005327D02* +X021413Y005327D01* +X021501Y005327D02* +X021501Y005857D01* +X021413Y005857D02* +X021589Y005857D01* +X018687Y005592D02* +X018333Y005592D01* +X018599Y005857D01* +X018599Y005327D01* +X018134Y005415D02* +X018046Y005327D01* +X017869Y005327D01* +X017781Y005415D01* +X017781Y005769D01* +X017869Y005857D01* +X018046Y005857D01* +X018134Y005769D01* +X017589Y005857D02* +X017413Y005857D01* +X017501Y005857D02* +X017501Y005327D01* +X017413Y005327D02* +X017589Y005327D01* +X014687Y005415D02* +X014599Y005327D01* +X014422Y005327D01* +X014333Y005415D01* +X014333Y005592D02* +X014510Y005681D01* +X014599Y005681D01* +X014687Y005592D01* +X014687Y005415D01* +X014333Y005592D02* +X014333Y005857D01* +X014687Y005857D01* +X014134Y005769D02* +X014046Y005857D01* +X013869Y005857D01* +X013781Y005769D01* +X013781Y005415D01* +X013869Y005327D01* +X014046Y005327D01* +X014134Y005415D01* +X013589Y005327D02* +X013413Y005327D01* +X013501Y005327D02* +X013501Y005857D01* +X013413Y005857D02* +X013589Y005857D01* +X010687Y005857D02* +X010510Y005769D01* +X010333Y005592D01* +X010599Y005592D01* +X010687Y005504D01* +X010687Y005415D01* +X010599Y005327D01* +X010422Y005327D01* +X010333Y005415D01* +X010333Y005592D01* +X010134Y005415D02* +X010046Y005327D01* +X009869Y005327D01* +X009781Y005415D01* +X009781Y005769D01* +X009869Y005857D01* +X010046Y005857D01* +X010134Y005769D01* +X009589Y005857D02* +X009413Y005857D01* +X009501Y005857D02* +X009501Y005327D01* +X009413Y005327D02* +X009589Y005327D01* +X007366Y006725D02* +X007366Y006902D01* +X007277Y006991D01* +X007277Y007189D02* +X007366Y007278D01* +X007366Y007455D01* +X007277Y007543D01* +X007101Y007543D01* +X007012Y007455D01* +X007012Y007366D01* +X007101Y007189D01* +X006835Y007189D01* +X006835Y007543D01* +X006924Y006991D02* +X006835Y006902D01* +X006835Y006725D01* +X006924Y006637D01* +X007277Y006637D01* +X007366Y006725D01* +X005227Y009537D02* +X005316Y009625D01* +X005316Y009802D01* +X005227Y009891D01* +X005139Y009891D01* +X005051Y009802D01* +X005051Y009625D01* +X004962Y009537D01* +X004874Y009537D01* +X004785Y009625D01* +X004785Y009802D01* +X004874Y009891D01* +X004785Y010089D02* +X004785Y010443D01* +X004785Y010266D02* +X005316Y010266D01* +X005316Y010642D02* +X005316Y010995D01* +X005316Y010819D02* +X004785Y010819D01* +X004962Y010642D01* +X006924Y012637D02* +X006835Y012725D01* +X006835Y012902D01* +X006924Y012991D01* +X007101Y013189D02* +X007101Y013455D01* +X007189Y013543D01* +X007277Y013543D01* +X007366Y013455D01* +X007366Y013278D01* +X007277Y013189D01* +X007101Y013189D01* +X006924Y013366D01* +X006835Y013543D01* +X007277Y012991D02* +X007366Y012902D01* +X007366Y012725D01* +X007277Y012637D01* +X006924Y012637D01* +X011913Y019077D02* +X012089Y019077D01* +X012001Y019077D02* +X012001Y019607D01* +X011913Y019607D02* +X012089Y019607D01* +X012281Y019519D02* +X012281Y019165D01* +X012369Y019077D01* +X012546Y019077D01* +X012634Y019165D01* +X012833Y019165D02* +X012922Y019077D01* +X013099Y019077D01* +X013187Y019165D01* +X013187Y019519D01* +X013099Y019607D01* +X012922Y019607D01* +X012833Y019519D01* +X012833Y019431D01* +X012922Y019342D01* +X013187Y019342D01* +X012634Y019519D02* +X012546Y019607D01* +X012369Y019607D01* +X012281Y019519D01* +X020413Y019607D02* +X020589Y019607D01* +X020501Y019607D02* +X020501Y019077D01* +X020413Y019077D02* +X020589Y019077D01* +X020781Y019165D02* +X020869Y019077D01* +X021046Y019077D01* +X021134Y019165D01* +X021333Y019165D02* +X021333Y019254D01* +X021422Y019342D01* +X021599Y019342D01* +X021687Y019254D01* +X021687Y019165D01* +X021599Y019077D01* +X021422Y019077D01* +X021333Y019165D01* +X021422Y019342D02* +X021333Y019431D01* +X021333Y019519D01* +X021422Y019607D01* +X021599Y019607D01* +X021687Y019519D01* +X021687Y019431D01* +X021599Y019342D01* +X021134Y019519D02* +X021046Y019607D01* +X020869Y019607D01* +X020781Y019519D01* +X020781Y019165D01* +X020677Y025137D02* +X020324Y025137D01* +X020235Y025225D01* +X020235Y025402D01* +X020324Y025491D01* +X020412Y025689D02* +X020235Y025866D01* +X020766Y025866D01* +X020766Y025689D02* +X020766Y026043D01* +X020677Y025491D02* +X020766Y025402D01* +X020766Y025225D01* +X020677Y025137D01* +X021565Y026684D02* +X021565Y026861D01* +X021565Y026772D02* +X022007Y026772D01* +X022096Y026684D01* +X022096Y026595D01* +X022007Y026507D01* +X021919Y027059D02* +X021919Y027325D01* +X021831Y027413D01* +X021654Y027413D01* +X021565Y027325D01* +X021565Y027059D01* +X022096Y027059D01* +X022096Y027612D02* +X022096Y027965D01* +X022096Y027789D02* +X021565Y027789D01* +X021742Y027612D01* +X023185Y027666D02* +X023716Y027666D01* +X023716Y027489D02* +X023716Y027843D01* +X023362Y027489D02* +X023185Y027666D01* +X023274Y027291D02* +X023185Y027202D01* +X023185Y026937D01* +X023716Y026937D01* +X023716Y027202D01* +X023627Y027291D01* +X023539Y027291D01* +X023451Y027202D01* +X023451Y026937D01* +X023451Y027202D02* +X023362Y027291D01* +X023274Y027291D01* +X031285Y025505D02* +X031285Y025328D01* +X031374Y025239D01* +X031374Y025041D02* +X031285Y024952D01* +X031285Y024775D01* +X031374Y024687D01* +X031727Y024687D01* +X031816Y024775D01* +X031816Y024952D01* +X031727Y025041D01* +X031816Y025239D02* +X031462Y025593D01* +X031374Y025593D01* +X031285Y025505D01* +X031816Y025593D02* +X031816Y025239D01* +X032535Y022745D02* +X032624Y022569D01* +X032801Y022392D01* +X032801Y022657D01* +X032889Y022745D01* +X032977Y022745D01* +X033066Y022657D01* +X033066Y022480D01* +X032977Y022392D01* +X032801Y022392D01* +X032977Y022193D02* +X032535Y022193D01* +X032535Y021839D02* +X032977Y021839D01* +X033066Y021928D01* +X033066Y022105D01* +X032977Y022193D01* +X032977Y021641D02* +X033066Y021552D01* +X033066Y021287D01* +X032535Y021287D01* +X032535Y021552D01* +X032624Y021641D01* +X032712Y021641D01* +X032801Y021552D01* +X032801Y021287D01* +X032801Y021552D02* +X032889Y021641D01* +X032977Y021641D01* +X035235Y020855D02* +X035235Y020678D01* +X035324Y020589D01* +X035235Y020391D02* +X035677Y020391D01* +X035766Y020302D01* +X035766Y020125D01* +X035677Y020037D01* +X035235Y020037D01* +X035766Y020589D02* +X035412Y020943D01* +X035324Y020943D01* +X035235Y020855D01* +X035766Y020943D02* +X035766Y020589D01* +X033616Y018455D02* +X033527Y018543D01* +X033439Y018543D01* +X033351Y018455D01* +X033351Y018366D01* +X033351Y018455D02* +X033262Y018543D01* +X033174Y018543D01* +X033085Y018455D01* +X033085Y018278D01* +X033174Y018189D01* +X033174Y017991D02* +X033085Y017902D01* +X033085Y017725D01* +X033174Y017637D01* +X033527Y017637D01* +X033616Y017725D01* +X033616Y017902D01* +X033527Y017991D01* +X033527Y018189D02* +X033616Y018278D01* +X033616Y018455D01* +X030187Y019519D02* +X029833Y019165D01* +X029833Y019077D01* +X029634Y019165D02* +X029546Y019077D01* +X029369Y019077D01* +X029281Y019165D01* +X029281Y019519D01* +X029369Y019607D01* +X029546Y019607D01* +X029634Y019519D01* +X029833Y019607D02* +X030187Y019607D01* +X030187Y019519D01* +X029089Y019607D02* +X028913Y019607D01* +X029001Y019607D02* +X029001Y019077D01* +X028913Y019077D02* +X029089Y019077D01* +X031085Y015705D02* +X031351Y015439D01* +X031351Y015793D01* +X031616Y015705D02* +X031085Y015705D01* +X031174Y015241D02* +X031085Y015152D01* +X031085Y014975D01* +X031174Y014887D01* +X031527Y014887D01* +X031616Y014975D01* +X031616Y015152D01* +X031527Y015241D01* +X033585Y011566D02* +X034116Y011566D01* +X034116Y011389D02* +X034116Y011743D01* +X033762Y011389D02* +X033585Y011566D01* +X033674Y011191D02* +X033585Y011102D01* +X033585Y010837D01* +X034116Y010837D01* +X034116Y011102D01* +X034027Y011191D01* +X033674Y011191D01* +X034466Y001243D02* +X034466Y000889D01* +X034466Y001066D02* +X033935Y001066D01* +X034112Y000889D01* +X034024Y000691D02* +X034201Y000691D01* +X034289Y000602D01* +X034289Y000337D01* +X034289Y000514D02* +X034466Y000691D01* +X034466Y000337D02* +X033935Y000337D01* +X033935Y000602D01* +X034024Y000691D01* +X034935Y000602D02* +X035024Y000691D01* +X035201Y000691D01* +X035289Y000602D01* +X035289Y000337D01* +X035289Y000514D02* +X035466Y000691D01* +X035466Y000889D02* +X035112Y001243D01* +X035024Y001243D01* +X034935Y001155D01* +X034935Y000978D01* +X035024Y000889D01* +X034935Y000602D02* +X034935Y000337D01* +X035466Y000337D01* +X035935Y000337D02* +X035935Y000602D01* +X036024Y000691D01* +X036201Y000691D01* +X036289Y000602D01* +X036289Y000337D01* +X036289Y000514D02* +X036466Y000691D01* +X036377Y000889D02* +X036466Y000978D01* +X036466Y001155D01* +X036377Y001243D01* +X036289Y001243D01* +X036201Y001155D01* +X036201Y001066D01* +X036201Y001155D02* +X036112Y001243D01* +X036024Y001243D01* +X035935Y001155D01* +X035935Y000978D01* +X036024Y000889D01* +X035466Y000889D02* +X035466Y001243D01* +X035935Y000337D02* +X036466Y000337D01* +X036935Y000337D02* +X036935Y000602D01* +X037024Y000691D01* +X037201Y000691D01* +X037289Y000602D01* +X037289Y000337D01* +X037289Y000514D02* +X037466Y000691D01* +X037201Y000889D02* +X037201Y001243D01* +X037466Y001155D02* +X036935Y001155D01* +X037201Y000889D01* +X037466Y000337D02* +X036935Y000337D01* +X037935Y000337D02* +X037935Y000602D01* +X038024Y000691D01* +X038201Y000691D01* +X038289Y000602D01* +X038289Y000337D01* +X038289Y000514D02* +X038466Y000691D01* +X038377Y000889D02* +X038466Y000978D01* +X038466Y001155D01* +X038377Y001243D01* +X038201Y001243D01* +X038112Y001155D01* +X038112Y001066D01* +X038201Y000889D01* +X037935Y000889D01* +X037935Y001243D01* +X037935Y000337D02* +X038466Y000337D01* +X035677Y024537D02* +X035235Y024537D01* +X035235Y024891D02* +X035677Y024891D01* +X035766Y024802D01* +X035766Y024625D01* +X035677Y024537D01* +X035766Y025089D02* +X035766Y025443D01* +X035766Y025266D02* +X035235Y025266D01* +X035412Y025089D01* +X005316Y028142D02* +X005211Y028037D01* +X005316Y028142D02* +X005316Y028352D01* +X005211Y028457D01* +X005106Y028457D01* +X005001Y028352D01* +X005001Y028142D01* +X004895Y028037D01* +X004790Y028037D01* +X004685Y028142D01* +X004685Y028352D01* +X004790Y028457D01* +X004685Y028681D02* +X004685Y029102D01* +X004685Y028892D02* +X005316Y028892D01* +X005316Y029326D02* +X004895Y029746D01* +X004790Y029746D01* +X004685Y029641D01* +X004685Y029431D01* +X004790Y029326D01* +X005316Y029326D02* +X005316Y029746D01* +D13* +X003701Y030602D02* +X001001Y030602D01* +X001001Y027802D01* +X001001Y027202D02* +X001001Y023802D01* +X001001Y022202D02* +X001001Y017802D01* +X001001Y017202D02* +X001001Y015402D01* +X003701Y015402D01* +X003701Y030602D01* +X003701Y011602D02* +X001001Y011602D01* +X003701Y011602D02* +X003701Y001402D01* +X001001Y001402D01* +X001001Y003202D01* +X001001Y003802D02* +X001001Y005702D01* +X034001Y004652D02* +X034701Y004652D01* +X034701Y004552D02* +X034001Y004552D01* +X035001Y004552D02* +X035701Y004552D01* +X035701Y004652D02* +X035001Y004652D01* +X036001Y004652D02* +X036701Y004652D01* +X036701Y004552D02* +X036001Y004552D01* +X037001Y004552D02* +X037701Y004552D01* +X037701Y004652D02* +X037001Y004652D01* +X038001Y004652D02* +X038701Y004652D01* +X038701Y004552D02* +X038001Y004552D01* +X038201Y006652D02* +X035501Y006652D01* +X035501Y016852D01* +X038201Y016852D01* +X038201Y015052D01* +X038201Y014452D02* +X038201Y012552D01* +X039351Y018702D02* +X042601Y018702D01* +X042601Y022302D01* +X039351Y022302D01* +X039351Y018702D01* +X039351Y023202D02* +X042601Y023202D01* +X042601Y026802D01* +X039351Y026802D01* +X039351Y023202D01* +X038701Y002452D02* +X038001Y002452D01* +X038001Y002352D02* +X038701Y002352D01* +X037701Y002352D02* +X037001Y002352D01* +X037001Y002452D02* +X037701Y002452D01* +X036701Y002452D02* +X036001Y002452D01* +X036001Y002352D02* +X036701Y002352D01* +X035701Y002352D02* +X035001Y002352D01* +X035001Y002452D02* +X035701Y002452D01* +X034701Y002452D02* +X034001Y002452D01* +X034001Y002352D02* +X034701Y002352D01* +D14* +X033170Y028706D02* +X033130Y028706D01* +X033131Y028707D02* +X033128Y028733D01* +X033122Y028759D01* +X033112Y028783D01* +X033098Y028806D01* +X033081Y028826D01* +X033062Y028844D01* +X033040Y028859D01* +X033016Y028870D01* +X032990Y028878D01* +X032964Y028882D01* +X032938Y028882D01* +X032912Y028878D01* +X032886Y028870D01* +X032862Y028859D01* +X032840Y028844D01* +X032821Y028826D01* +X032804Y028806D01* +X032790Y028783D01* +X032780Y028759D01* +X032774Y028733D01* +X032771Y028707D01* +X032732Y028706D01* +X032731Y028707D01* +X032734Y028736D01* +X032740Y028765D01* +X032751Y028793D01* +X032765Y028819D01* +X032782Y028843D01* +X032803Y028864D01* +X032826Y028883D01* +X032851Y028898D01* +X032878Y028910D01* +X032907Y028918D01* +X032936Y028922D01* +X032966Y028922D01* +X032995Y028918D01* +X033024Y028910D01* +X033051Y028898D01* +X033076Y028883D01* +X033099Y028864D01* +X033120Y028843D01* +X033137Y028819D01* +X033151Y028793D01* +X033162Y028765D01* +X033168Y028736D01* +X033171Y028707D01* +X033162Y028707D01* +X033159Y028735D01* +X033153Y028763D01* +X033143Y028789D01* +X033130Y028814D01* +X033113Y028837D01* +X033093Y028858D01* +X033071Y028876D01* +X033047Y028890D01* +X033021Y028901D01* +X032993Y028909D01* +X032965Y028913D01* +X032937Y028913D01* +X032909Y028909D01* +X032881Y028901D01* +X032855Y028890D01* +X032831Y028876D01* +X032809Y028858D01* +X032789Y028837D01* +X032772Y028814D01* +X032759Y028789D01* +X032749Y028763D01* +X032743Y028735D01* +X032740Y028707D01* +X032749Y028707D01* +X032752Y028735D01* +X032758Y028763D01* +X032769Y028789D01* +X032783Y028814D01* +X032800Y028836D01* +X032820Y028856D01* +X032843Y028873D01* +X032868Y028886D01* +X032895Y028896D01* +X032923Y028902D01* +X032951Y028904D01* +X032979Y028902D01* +X033007Y028896D01* +X033034Y028886D01* +X033059Y028873D01* +X033082Y028856D01* +X033102Y028836D01* +X033119Y028814D01* +X033133Y028789D01* +X033144Y028763D01* +X033150Y028735D01* +X033153Y028707D01* +X033144Y028707D01* +X033141Y028734D01* +X033135Y028760D01* +X033125Y028785D01* +X033112Y028809D01* +X033095Y028830D01* +X033076Y028849D01* +X033054Y028865D01* +X033030Y028878D01* +X033005Y028887D01* +X032978Y028893D01* +X032951Y028895D01* +X032924Y028893D01* +X032897Y028887D01* +X032872Y028878D01* +X032848Y028865D01* +X032826Y028849D01* +X032807Y028830D01* +X032790Y028809D01* +X032777Y028785D01* +X032767Y028760D01* +X032761Y028734D01* +X032758Y028707D01* +X032767Y028707D01* +X032770Y028734D01* +X032776Y028760D01* +X032787Y028785D01* +X032801Y028808D01* +X032818Y028829D01* +X032838Y028847D01* +X032860Y028862D01* +X032885Y028874D01* +X032911Y028882D01* +X032937Y028886D01* +X032965Y028886D01* +X032991Y028882D01* +X033017Y028874D01* +X033042Y028862D01* +X033064Y028847D01* +X033084Y028829D01* +X033101Y028808D01* +X033115Y028785D01* +X033126Y028760D01* +X033132Y028734D01* +X033135Y028707D01* +X032332Y028698D02* +X032372Y028698D01* +X032371Y028697D02* +X032374Y028671D01* +X032380Y028645D01* +X032390Y028621D01* +X032404Y028598D01* +X032421Y028578D01* +X032440Y028560D01* +X032462Y028545D01* +X032486Y028534D01* +X032512Y028526D01* +X032538Y028522D01* +X032564Y028522D01* +X032590Y028526D01* +X032616Y028534D01* +X032640Y028545D01* +X032662Y028560D01* +X032681Y028578D01* +X032698Y028598D01* +X032712Y028621D01* +X032722Y028645D01* +X032728Y028671D01* +X032731Y028697D01* +X032770Y028698D01* +X032771Y028697D01* +X032768Y028668D01* +X032762Y028639D01* +X032751Y028611D01* +X032737Y028585D01* +X032720Y028561D01* +X032699Y028540D01* +X032676Y028521D01* +X032651Y028506D01* +X032624Y028494D01* +X032595Y028486D01* +X032566Y028482D01* +X032536Y028482D01* +X032507Y028486D01* +X032478Y028494D01* +X032451Y028506D01* +X032426Y028521D01* +X032403Y028540D01* +X032382Y028561D01* +X032365Y028585D01* +X032351Y028611D01* +X032340Y028639D01* +X032334Y028668D01* +X032331Y028697D01* +X032340Y028697D01* +X032343Y028669D01* +X032349Y028641D01* +X032359Y028615D01* +X032372Y028590D01* +X032389Y028567D01* +X032409Y028546D01* +X032431Y028528D01* +X032455Y028514D01* +X032481Y028503D01* +X032509Y028495D01* +X032537Y028491D01* +X032565Y028491D01* +X032593Y028495D01* +X032621Y028503D01* +X032647Y028514D01* +X032671Y028528D01* +X032693Y028546D01* +X032713Y028567D01* +X032730Y028590D01* +X032743Y028615D01* +X032753Y028641D01* +X032759Y028669D01* +X032762Y028697D01* +X032753Y028697D01* +X032750Y028669D01* +X032744Y028641D01* +X032733Y028615D01* +X032719Y028590D01* +X032702Y028568D01* +X032682Y028548D01* +X032659Y028531D01* +X032634Y028518D01* +X032607Y028508D01* +X032579Y028502D01* +X032551Y028500D01* +X032523Y028502D01* +X032495Y028508D01* +X032468Y028518D01* +X032443Y028531D01* +X032420Y028548D01* +X032400Y028568D01* +X032383Y028590D01* +X032369Y028615D01* +X032358Y028641D01* +X032352Y028669D01* +X032349Y028697D01* +X032358Y028697D01* +X032361Y028670D01* +X032367Y028644D01* +X032377Y028619D01* +X032390Y028595D01* +X032407Y028574D01* +X032426Y028555D01* +X032448Y028539D01* +X032472Y028526D01* +X032497Y028517D01* +X032524Y028511D01* +X032551Y028509D01* +X032578Y028511D01* +X032605Y028517D01* +X032630Y028526D01* +X032654Y028539D01* +X032676Y028555D01* +X032695Y028574D01* +X032712Y028595D01* +X032725Y028619D01* +X032735Y028644D01* +X032741Y028670D01* +X032744Y028697D01* +X032735Y028697D01* +X032732Y028670D01* +X032726Y028644D01* +X032715Y028619D01* +X032701Y028596D01* +X032684Y028575D01* +X032664Y028557D01* +X032642Y028542D01* +X032617Y028530D01* +X032591Y028522D01* +X032565Y028518D01* +X032537Y028518D01* +X032511Y028522D01* +X032485Y028530D01* +X032460Y028542D01* +X032438Y028557D01* +X032418Y028575D01* +X032401Y028596D01* +X032387Y028619D01* +X032376Y028644D01* +X032370Y028670D01* +X032367Y028697D01* +X030170Y028706D02* +X030130Y028706D01* +X030131Y028707D02* +X030128Y028733D01* +X030122Y028759D01* +X030112Y028783D01* +X030098Y028806D01* +X030081Y028826D01* +X030062Y028844D01* +X030040Y028859D01* +X030016Y028870D01* +X029990Y028878D01* +X029964Y028882D01* +X029938Y028882D01* +X029912Y028878D01* +X029886Y028870D01* +X029862Y028859D01* +X029840Y028844D01* +X029821Y028826D01* +X029804Y028806D01* +X029790Y028783D01* +X029780Y028759D01* +X029774Y028733D01* +X029771Y028707D01* +X029732Y028706D01* +X029731Y028707D01* +X029734Y028736D01* +X029740Y028765D01* +X029751Y028793D01* +X029765Y028819D01* +X029782Y028843D01* +X029803Y028864D01* +X029826Y028883D01* +X029851Y028898D01* +X029878Y028910D01* +X029907Y028918D01* +X029936Y028922D01* +X029966Y028922D01* +X029995Y028918D01* +X030024Y028910D01* +X030051Y028898D01* +X030076Y028883D01* +X030099Y028864D01* +X030120Y028843D01* +X030137Y028819D01* +X030151Y028793D01* +X030162Y028765D01* +X030168Y028736D01* +X030171Y028707D01* +X030162Y028707D01* +X030159Y028735D01* +X030153Y028763D01* +X030143Y028789D01* +X030130Y028814D01* +X030113Y028837D01* +X030093Y028858D01* +X030071Y028876D01* +X030047Y028890D01* +X030021Y028901D01* +X029993Y028909D01* +X029965Y028913D01* +X029937Y028913D01* +X029909Y028909D01* +X029881Y028901D01* +X029855Y028890D01* +X029831Y028876D01* +X029809Y028858D01* +X029789Y028837D01* +X029772Y028814D01* +X029759Y028789D01* +X029749Y028763D01* +X029743Y028735D01* +X029740Y028707D01* +X029749Y028707D01* +X029752Y028735D01* +X029758Y028763D01* +X029769Y028789D01* +X029783Y028814D01* +X029800Y028836D01* +X029820Y028856D01* +X029843Y028873D01* +X029868Y028886D01* +X029895Y028896D01* +X029923Y028902D01* +X029951Y028904D01* +X029979Y028902D01* +X030007Y028896D01* +X030034Y028886D01* +X030059Y028873D01* +X030082Y028856D01* +X030102Y028836D01* +X030119Y028814D01* +X030133Y028789D01* +X030144Y028763D01* +X030150Y028735D01* +X030153Y028707D01* +X030144Y028707D01* +X030141Y028734D01* +X030135Y028760D01* +X030125Y028785D01* +X030112Y028809D01* +X030095Y028830D01* +X030076Y028849D01* +X030054Y028865D01* +X030030Y028878D01* +X030005Y028887D01* +X029978Y028893D01* +X029951Y028895D01* +X029924Y028893D01* +X029897Y028887D01* +X029872Y028878D01* +X029848Y028865D01* +X029826Y028849D01* +X029807Y028830D01* +X029790Y028809D01* +X029777Y028785D01* +X029767Y028760D01* +X029761Y028734D01* +X029758Y028707D01* +X029767Y028707D01* +X029770Y028734D01* +X029776Y028760D01* +X029787Y028785D01* +X029801Y028808D01* +X029818Y028829D01* +X029838Y028847D01* +X029860Y028862D01* +X029885Y028874D01* +X029911Y028882D01* +X029937Y028886D01* +X029965Y028886D01* +X029991Y028882D01* +X030017Y028874D01* +X030042Y028862D01* +X030064Y028847D01* +X030084Y028829D01* +X030101Y028808D01* +X030115Y028785D01* +X030126Y028760D01* +X030132Y028734D01* +X030135Y028707D01* +X029332Y028698D02* +X029372Y028698D01* +X029371Y028697D02* +X029374Y028671D01* +X029380Y028645D01* +X029390Y028621D01* +X029404Y028598D01* +X029421Y028578D01* +X029440Y028560D01* +X029462Y028545D01* +X029486Y028534D01* +X029512Y028526D01* +X029538Y028522D01* +X029564Y028522D01* +X029590Y028526D01* +X029616Y028534D01* +X029640Y028545D01* +X029662Y028560D01* +X029681Y028578D01* +X029698Y028598D01* +X029712Y028621D01* +X029722Y028645D01* +X029728Y028671D01* +X029731Y028697D01* +X029770Y028698D01* +X029771Y028697D01* +X029768Y028668D01* +X029762Y028639D01* +X029751Y028611D01* +X029737Y028585D01* +X029720Y028561D01* +X029699Y028540D01* +X029676Y028521D01* +X029651Y028506D01* +X029624Y028494D01* +X029595Y028486D01* +X029566Y028482D01* +X029536Y028482D01* +X029507Y028486D01* +X029478Y028494D01* +X029451Y028506D01* +X029426Y028521D01* +X029403Y028540D01* +X029382Y028561D01* +X029365Y028585D01* +X029351Y028611D01* +X029340Y028639D01* +X029334Y028668D01* +X029331Y028697D01* +X029340Y028697D01* +X029343Y028669D01* +X029349Y028641D01* +X029359Y028615D01* +X029372Y028590D01* +X029389Y028567D01* +X029409Y028546D01* +X029431Y028528D01* +X029455Y028514D01* +X029481Y028503D01* +X029509Y028495D01* +X029537Y028491D01* +X029565Y028491D01* +X029593Y028495D01* +X029621Y028503D01* +X029647Y028514D01* +X029671Y028528D01* +X029693Y028546D01* +X029713Y028567D01* +X029730Y028590D01* +X029743Y028615D01* +X029753Y028641D01* +X029759Y028669D01* +X029762Y028697D01* +X029753Y028697D01* +X029750Y028669D01* +X029744Y028641D01* +X029733Y028615D01* +X029719Y028590D01* +X029702Y028568D01* +X029682Y028548D01* +X029659Y028531D01* +X029634Y028518D01* +X029607Y028508D01* +X029579Y028502D01* +X029551Y028500D01* +X029523Y028502D01* +X029495Y028508D01* +X029468Y028518D01* +X029443Y028531D01* +X029420Y028548D01* +X029400Y028568D01* +X029383Y028590D01* +X029369Y028615D01* +X029358Y028641D01* +X029352Y028669D01* +X029349Y028697D01* +X029358Y028697D01* +X029361Y028670D01* +X029367Y028644D01* +X029377Y028619D01* +X029390Y028595D01* +X029407Y028574D01* +X029426Y028555D01* +X029448Y028539D01* +X029472Y028526D01* +X029497Y028517D01* +X029524Y028511D01* +X029551Y028509D01* +X029578Y028511D01* +X029605Y028517D01* +X029630Y028526D01* +X029654Y028539D01* +X029676Y028555D01* +X029695Y028574D01* +X029712Y028595D01* +X029725Y028619D01* +X029735Y028644D01* +X029741Y028670D01* +X029744Y028697D01* +X029735Y028697D01* +X029732Y028670D01* +X029726Y028644D01* +X029715Y028619D01* +X029701Y028596D01* +X029684Y028575D01* +X029664Y028557D01* +X029642Y028542D01* +X029617Y028530D01* +X029591Y028522D01* +X029565Y028518D01* +X029537Y028518D01* +X029511Y028522D01* +X029485Y028530D01* +X029460Y028542D01* +X029438Y028557D01* +X029418Y028575D01* +X029401Y028596D01* +X029387Y028619D01* +X029376Y028644D01* +X029370Y028670D01* +X029367Y028697D01* +D15* +X030101Y025382D02* +X030351Y025382D01* +X030601Y025382D01* +X030601Y025132D02* +X030351Y025132D01* +X030101Y025132D01* +X019251Y024352D02* +X018701Y024352D01* +X018751Y024302D02* +X019201Y024302D01* +X018551Y024452D02* +X018301Y024452D01* +X018301Y024552D02* +X018551Y024552D01* +X018151Y024352D02* +X007451Y024352D01* +X007501Y024302D02* +X018101Y024302D01* +X018301Y030552D02* +X018551Y030552D01* +X018751Y030702D02* +X019201Y030702D01* +X018101Y030702D02* +X007501Y030702D01* +D16* +X037876Y026002D03* +X037876Y025002D03* +X037876Y024002D03* +X037876Y021502D03* +X037876Y020502D03* +X037876Y019502D03* +D17* +X038551Y019502D03* +X038551Y020502D03* +X038551Y021502D03* +X038551Y024002D03* +X038551Y025002D03* +X038551Y026002D03* +D18* +X038351Y004922D03* +X038351Y004922D03* +X037351Y004922D03* +X037351Y004922D03* +X036351Y004922D03* +X035351Y004922D03* +X035351Y004922D03* +X034351Y004922D03* +X034351Y004922D03* +X034351Y002082D03* +X034351Y002082D03* +X035351Y002082D03* +X035351Y002082D03* +X036351Y002082D03* +X037351Y002082D03* +X037351Y002082D03* +X038351Y002082D03* +X038351Y002082D03* +D19* +X032851Y010952D03* +D20* +X032851Y010327D03* +X032851Y012677D03* +D21* +X016901Y027502D03* +D22* +X019526Y027502D03* +X007176Y027502D03* +M02* diff --git a/testdata/hexapod.sch b/testdata/hexapod.sch new file mode 100644 index 0000000..59a9c46 Binary files /dev/null and b/testdata/hexapod.sch differ diff --git a/testdata/hexapod.sol b/testdata/hexapod.sol new file mode 100644 index 0000000..c6bc45a --- /dev/null +++ b/testdata/hexapod.sol @@ -0,0 +1,1413 @@ +G75* +G70* +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0070*% +%ADD11C,0.0630*% +%ADD12C,0.1240*% +%ADD13C,0.0700*% +%ADD14O,0.1575X0.0787*% +%ADD15C,0.0240*% +%ADD16C,0.0850*% +%ADD17C,0.0160*% +%ADD18C,0.0472*% +%ADD19C,0.0500*% +%ADD20C,0.1000*% +%ADD21C,0.0600*% +D10* +X001567Y013287D02* +X001567Y013917D01* +X001777Y013917D02* +X001356Y013917D01* +X002001Y013812D02* +X002001Y013392D01* +X002106Y013287D01* +X002316Y013287D01* +X002421Y013392D01* +X002421Y013812D01* +X002316Y013917D01* +X002106Y013917D01* +X002001Y013812D01* +X002645Y013812D02* +X002645Y013707D01* +X002751Y013602D01* +X003066Y013602D01* +X003066Y013287D02* +X002751Y013287D01* +X002645Y013392D01* +X002645Y013497D01* +X002751Y013602D01* +X002645Y013812D02* +X002751Y013917D01* +X003066Y013917D01* +X003066Y013287D01* +D11* +X005601Y013502D03* +X005601Y012502D03* +X008351Y012502D03* +X008351Y013502D03* +X008351Y011502D03* +X008351Y010502D03* +X008351Y009502D03* +X008351Y008502D03* +X008351Y007502D03* +X008351Y006502D03* +X008851Y004502D03* +X009851Y004502D03* +X010851Y004502D03* +X011851Y004502D03* +X012851Y004502D03* +X013851Y004502D03* +X014851Y004502D03* +X015851Y004502D03* +X016851Y004502D03* +X017851Y004502D03* +X018851Y004502D03* +X019851Y004502D03* +X020851Y004502D03* +X021851Y004502D03* +X022851Y004502D03* +X023851Y004502D03* +X024851Y004502D03* +X025851Y004502D03* +X026851Y004502D03* +X027851Y004502D03* +X028851Y004502D03* +X029851Y004502D03* +X030851Y004502D03* +X031851Y004502D03* +X031851Y003502D03* +X030851Y003502D03* +X029851Y003502D03* +X028851Y003502D03* +X027851Y003502D03* +X026851Y003502D03* +X025851Y003502D03* +X024851Y003502D03* +X023851Y003502D03* +X022851Y003502D03* +X021851Y003502D03* +X020851Y003502D03* +X019851Y003502D03* +X018851Y003502D03* +X017851Y003502D03* +X016851Y003502D03* +X015851Y003502D03* +X014851Y003502D03* +X013851Y003502D03* +X012851Y003502D03* +X011851Y003502D03* +X010851Y003502D03* +X009851Y003502D03* +X008851Y003502D03* +X007851Y003502D03* +X007851Y004502D03* +X005601Y006502D03* +X005601Y007502D03* +X002851Y008002D03* +X001851Y008002D03* +X001851Y009002D03* +X002851Y009002D03* +X002851Y010002D03* +X001851Y010002D03* +X001851Y007002D03* +X002851Y007002D03* +X002851Y006002D03* +X001851Y006002D03* +X001851Y005002D03* +X002851Y005002D03* +X002851Y004002D03* +X001851Y004002D03* +X001851Y003002D03* +X002851Y003002D03* +X011351Y006502D03* +X012351Y006502D03* +X012351Y007502D03* +X012351Y008502D03* +X012351Y009502D03* +X012351Y010502D03* +X011351Y010502D03* +X011351Y009502D03* +X011351Y008502D03* +X011351Y007502D03* +X015351Y007502D03* +X016351Y007502D03* +X016351Y008502D03* +X015351Y008502D03* +X015351Y009502D03* +X016351Y009502D03* +X016351Y010502D03* +X015351Y010502D03* +X015351Y011502D03* +X016351Y011502D03* +X016351Y012502D03* +X015351Y012502D03* +X015351Y013502D03* +X016351Y013502D03* +X016351Y015502D03* +X015351Y015502D03* +X014351Y015502D03* +X012851Y015502D03* +X011851Y015502D03* +X010851Y015502D03* +X009851Y015502D03* +X008851Y015502D03* +X007851Y015502D03* +X006851Y015502D03* +X005851Y015502D03* +X002851Y017002D03* +X001851Y017002D03* +X001851Y018002D03* +X002851Y018002D03* +X002851Y019002D03* +X001851Y019002D03* +X001851Y020002D03* +X002851Y020002D03* +X002851Y021002D03* +X001851Y021002D03* +X001851Y022002D03* +X002851Y022002D03* +X002851Y023002D03* +X001851Y023002D03* +X001851Y024002D03* +X002851Y024002D03* +X002851Y025002D03* +X001851Y025002D03* +X001851Y026002D03* +X002851Y026002D03* +X002851Y027002D03* +X001851Y027002D03* +X001851Y028002D03* +X002851Y028002D03* +X002851Y029002D03* +X001851Y029002D03* +X007851Y022502D03* +X008851Y022502D03* +X009851Y022502D03* +X010851Y022502D03* +X011851Y022502D03* +X012851Y022502D03* +X013851Y022502D03* +X014851Y022502D03* +X015851Y022502D03* +X016851Y022502D03* +X017851Y022502D03* +X018851Y022502D03* +X019851Y022502D03* +X020851Y022502D03* +X021851Y022502D03* +X022851Y022502D03* +X023851Y022502D03* +X024851Y022502D03* +X025851Y022502D03* +X026851Y022502D03* +X027851Y022502D03* +X028851Y022502D03* +X029851Y022502D03* +X030851Y022502D03* +X031851Y022502D03* +X031851Y021502D03* +X030851Y021502D03* +X029851Y021502D03* +X028851Y021502D03* +X027851Y021502D03* +X026851Y021502D03* +X025851Y021502D03* +X024851Y021502D03* +X023851Y021502D03* +X022851Y021502D03* +X021851Y021502D03* +X020851Y021502D03* +X019851Y021502D03* +X018851Y021502D03* +X017851Y021502D03* +X016851Y021502D03* +X015851Y021502D03* +X014851Y021502D03* +X013851Y021502D03* +X012851Y021502D03* +X011851Y021502D03* +X010851Y021502D03* +X009851Y021502D03* +X008851Y021502D03* +X007851Y021502D03* +X007851Y018502D03* +X008851Y018502D03* +X009851Y018502D03* +X010851Y018502D03* +X011851Y018502D03* +X012851Y018502D03* +X014351Y018502D03* +X015351Y018502D03* +X016351Y018502D03* +X017351Y018502D03* +X018351Y018502D03* +X019351Y018502D03* +X020351Y018502D03* +X021351Y018502D03* +X022851Y018502D03* +X023851Y018502D03* +X024851Y018502D03* +X025851Y018502D03* +X026851Y018502D03* +X027851Y018502D03* +X028851Y018502D03* +X029851Y018502D03* +X031851Y018502D03* +X031851Y017502D03* +X032851Y015752D03* +X032851Y014752D03* +X031351Y013502D03* +X031351Y012502D03* +X031351Y011502D03* +X031351Y010502D03* +X031351Y009502D03* +X031351Y008502D03* +X031351Y007502D03* +X031351Y006502D03* +X028351Y006502D03* +X028351Y007502D03* +X028351Y008502D03* +X028351Y009502D03* +X028351Y010502D03* +X027351Y010502D03* +X027351Y009502D03* +X027351Y008502D03* +X027351Y007502D03* +X027351Y006502D03* +X024351Y006502D03* +X023351Y006502D03* +X023351Y007502D03* +X024351Y007502D03* +X024351Y008502D03* +X023351Y008502D03* +X023351Y009502D03* +X024351Y009502D03* +X024351Y010502D03* +X023351Y010502D03* +X023351Y011502D03* +X024351Y011502D03* +X024351Y012502D03* +X023351Y012502D03* +X023351Y013502D03* +X024351Y013502D03* +X024851Y015502D03* +X025851Y015502D03* +X026851Y015502D03* +X027851Y015502D03* +X028851Y015502D03* +X029851Y015502D03* +X028351Y013502D03* +X028351Y012502D03* +X028351Y011502D03* +X027351Y011502D03* +X027351Y012502D03* +X027351Y013502D03* +X023851Y015502D03* +X022851Y015502D03* +X021351Y015502D03* +X020351Y015502D03* +X019351Y015502D03* +X018351Y015502D03* +X017351Y015502D03* +X019351Y013502D03* +X020351Y013502D03* +X020351Y012502D03* +X019351Y012502D03* +X019351Y011502D03* +X020351Y011502D03* +X020351Y010502D03* +X019351Y010502D03* +X019351Y009502D03* +X020351Y009502D03* +X020351Y008502D03* +X019351Y008502D03* +X019351Y007502D03* +X020351Y007502D03* +X020351Y006502D03* +X019351Y006502D03* +X016351Y006502D03* +X015351Y006502D03* +X012351Y011502D03* +X012351Y012502D03* +X012351Y013502D03* +X011351Y013502D03* +X011351Y012502D03* +X011351Y011502D03* +X006851Y018502D03* +X005851Y018502D03* +X021601Y028752D03* +X021601Y029752D03* +X036351Y015252D03* +X037351Y015252D03* +X037351Y014252D03* +X036351Y014252D03* +X036351Y013252D03* +X037351Y013252D03* +X037351Y012252D03* +X036351Y012252D03* +X036351Y011252D03* +X037351Y011252D03* +X037351Y010252D03* +X036351Y010252D03* +X036351Y009252D03* +X037351Y009252D03* +X037351Y008252D03* +X036351Y008252D03* +X036351Y005502D03* +X037351Y005502D03* +X038351Y005502D03* +X035351Y005502D03* +X034351Y005502D03* +X034351Y001502D03* +X035351Y001502D03* +X036351Y001502D03* +X037351Y001502D03* +X038351Y001502D03* +D12* +X020351Y027502D03* +X025351Y029502D03* +X029291Y029502D03* +X032241Y029502D03* +X035191Y029502D03* +X006351Y027502D03* +D13* +X030351Y026252D03* +X030351Y024252D03* +D14* +X037101Y024002D03* +X037101Y025002D03* +X037101Y026002D03* +X037101Y021502D03* +X037101Y020502D03* +X037101Y019502D03* +D15* +X043751Y020502D02* +X043753Y020534D01* +X043759Y020565D01* +X043769Y020596D01* +X043783Y020625D01* +X043801Y020652D01* +X043821Y020676D01* +X043845Y020697D01* +X043871Y020716D01* +X043900Y020731D01* +X043930Y020742D01* +X043961Y020749D01* +X043993Y020752D01* +X044025Y020751D01* +X044057Y020746D01* +X044087Y020737D01* +X044117Y020724D01* +X044144Y020707D01* +X044169Y020687D01* +X044191Y020664D01* +X044211Y020638D01* +X044226Y020610D01* +X044238Y020581D01* +X044246Y020550D01* +X044250Y020518D01* +X044250Y020486D01* +X044246Y020454D01* +X044238Y020423D01* +X044226Y020394D01* +X044211Y020366D01* +X044191Y020340D01* +X044169Y020317D01* +X044144Y020297D01* +X044117Y020280D01* +X044087Y020267D01* +X044057Y020258D01* +X044025Y020253D01* +X043993Y020252D01* +X043961Y020255D01* +X043930Y020262D01* +X043900Y020273D01* +X043871Y020288D01* +X043845Y020307D01* +X043821Y020328D01* +X043801Y020352D01* +X043783Y020379D01* +X043769Y020408D01* +X043759Y020439D01* +X043753Y020470D01* +X043751Y020502D01* +X043751Y025002D02* +X043753Y025034D01* +X043759Y025065D01* +X043769Y025096D01* +X043783Y025125D01* +X043801Y025152D01* +X043821Y025176D01* +X043845Y025197D01* +X043871Y025216D01* +X043900Y025231D01* +X043930Y025242D01* +X043961Y025249D01* +X043993Y025252D01* +X044025Y025251D01* +X044057Y025246D01* +X044087Y025237D01* +X044117Y025224D01* +X044144Y025207D01* +X044169Y025187D01* +X044191Y025164D01* +X044211Y025138D01* +X044226Y025110D01* +X044238Y025081D01* +X044246Y025050D01* +X044250Y025018D01* +X044250Y024986D01* +X044246Y024954D01* +X044238Y024923D01* +X044226Y024894D01* +X044211Y024866D01* +X044191Y024840D01* +X044169Y024817D01* +X044144Y024797D01* +X044117Y024780D01* +X044087Y024767D01* +X044057Y024758D01* +X044025Y024753D01* +X043993Y024752D01* +X043961Y024755D01* +X043930Y024762D01* +X043900Y024773D01* +X043871Y024788D01* +X043845Y024807D01* +X043821Y024828D01* +X043801Y024852D01* +X043783Y024879D01* +X043769Y024908D01* +X043759Y024939D01* +X043753Y024970D01* +X043751Y025002D01* +D16* +X032851Y013502D03* +X032851Y009502D03* +D17* +X000851Y000752D02* +X000601Y001002D01* +X000601Y007752D01* +X000851Y008002D01* +X001851Y008002D01* +X002351Y007502D02* +X001101Y007502D01* +X000851Y007252D01* +X000851Y001252D01* +X001101Y001002D01* +X031601Y001002D01* +X033351Y002752D01* +X033851Y002252D02* +X032351Y000752D01* +X000851Y000752D01* +X001351Y001252D02* +X001101Y001502D01* +X001101Y006752D01* +X001351Y007002D01* +X001851Y007002D01* +X002351Y007502D02* +X002851Y008002D01* +X002851Y007002D02* +X003351Y007002D01* +X003601Y006752D01* +X003601Y006002D01* +X003851Y005752D01* +X004351Y005752D01* +X004601Y005502D01* +X004601Y003002D01* +X004851Y002752D01* +X012351Y002752D01* +X012601Y003002D01* +X018851Y003002D01* +X020101Y001752D01* +X029601Y001752D01* +X029851Y002002D01* +X031351Y002002D01* +X032101Y002752D02* +X030101Y002752D01* +X029851Y003002D01* +X029851Y003502D01* +X029351Y004002D02* +X028851Y004502D01* +X029351Y004002D02* +X033601Y004002D01* +X033851Y004252D01* +X033851Y011002D01* +X034101Y011252D01* +X036351Y011252D01* +X036851Y010752D02* +X034601Y010752D01* +X034351Y010502D01* +X034351Y005502D01* +X035351Y005502D02* +X035351Y009502D01* +X035601Y009752D01* +X036851Y009752D01* +X037351Y010252D01* +X036851Y010752D02* +X037351Y011252D01* +X036351Y010252D02* +X035101Y010252D01* +X034851Y010002D01* +X034851Y003502D01* +X034601Y003252D01* +X032601Y003252D01* +X032101Y002752D01* +X030351Y001502D02* +X030101Y001252D01* +X001351Y001252D01* +X003601Y002002D02* +X003601Y003752D01* +X003351Y004002D01* +X002851Y004002D01* +X002351Y004502D02* +X001851Y004002D01* +X002351Y004502D02* +X003601Y004502D01* +X003851Y004252D01* +X003851Y002252D01* +X004101Y002002D01* +X013351Y002002D01* +X013101Y002502D02* +X014601Y002502D01* +X015101Y002002D01* +X018101Y002002D01* +X018351Y002252D01* +X018351Y002502D01* +X017351Y002502D02* +X017101Y002752D01* +X012851Y002752D01* +X012601Y002502D01* +X004601Y002502D01* +X004351Y002752D01* +X004351Y005252D01* +X004101Y005502D01* +X002351Y005502D01* +X001851Y005002D01* +X002851Y005002D02* +X003851Y005002D01* +X004101Y004752D01* +X004101Y002502D01* +X004351Y002252D01* +X012851Y002252D01* +X013101Y002502D01* +X012851Y001752D02* +X003851Y001752D01* +X003601Y002002D01* +X009351Y007502D02* +X009851Y007002D01* +X011601Y007002D01* +X012101Y007502D01* +X012351Y007502D01* +X013351Y007502D01* +X013851Y007002D01* +X015601Y007002D01* +X016101Y007502D01* +X016351Y007502D01* +X017351Y007502D01* +X017851Y007002D01* +X019601Y007002D01* +X020101Y007502D01* +X020351Y007502D01* +X021351Y007502D01* +X021851Y007002D01* +X023601Y007002D01* +X024101Y007502D01* +X024351Y007502D01* +X025351Y007502D01* +X025851Y007002D01* +X026101Y007002D01* +X027601Y007002D01* +X028101Y007502D01* +X028351Y007502D01* +X028101Y008002D02* +X029351Y008002D01* +X029851Y007502D01* +X031351Y007502D01* +X031351Y008502D02* +X029851Y008502D01* +X029351Y009002D01* +X028101Y009002D01* +X027601Y008502D01* +X027351Y008502D01* +X025851Y008502D01* +X025351Y009002D01* +X024101Y009002D01* +X023601Y008502D01* +X023351Y008502D01* +X021851Y008502D01* +X021351Y009002D01* +X020101Y009002D01* +X019601Y008502D01* +X019351Y008502D01* +X017851Y008502D01* +X017351Y009002D01* +X016101Y009002D01* +X015601Y008502D01* +X015351Y008502D01* +X013851Y008502D01* +X013351Y009002D01* +X012101Y009002D01* +X011601Y008502D01* +X011351Y008502D01* +X011601Y008002D02* +X012101Y008502D01* +X012351Y008502D01* +X013351Y008502D01* +X013851Y008002D01* +X015601Y008002D01* +X016101Y008502D01* +X016351Y008502D01* +X017351Y008502D01* +X017851Y008002D01* +X019601Y008002D01* +X020101Y008502D01* +X020351Y008502D01* +X021351Y008502D01* +X021851Y008002D01* +X023601Y008002D01* +X024101Y008502D01* +X024351Y008502D01* +X025351Y008502D01* +X025851Y008002D01* +X026101Y008002D01* +X027601Y008002D01* +X028101Y008502D01* +X028351Y008502D01* +X028101Y008002D02* +X027601Y007502D01* +X027351Y007502D01* +X025851Y007502D01* +X025351Y008002D01* +X024101Y008002D01* +X023601Y007502D01* +X023351Y007502D01* +X021851Y007502D01* +X021351Y008002D01* +X020101Y008002D01* +X019601Y007502D01* +X019351Y007502D01* +X017851Y007502D01* +X017351Y008002D01* +X016101Y008002D01* +X015601Y007502D01* +X015351Y007502D01* +X013851Y007502D01* +X013351Y008002D01* +X012101Y008002D01* +X011601Y007502D01* +X011351Y007502D01* +X011601Y008002D02* +X009851Y008002D01* +X009351Y008502D01* +X008351Y008502D01* +X007851Y008502D02* +X007601Y008252D01* +X006601Y008252D01* +X006601Y009002D02* +X007351Y009002D01* +X007601Y009252D01* +X007601Y012502D01* +X008101Y013002D01* +X009351Y013002D01* +X009851Y012502D01* +X011351Y012502D01* +X011601Y013002D02* +X012101Y013502D01* +X012351Y013502D01* +X013351Y013502D01* +X013851Y013002D01* +X015601Y013002D01* +X016101Y013502D01* +X016351Y013502D01* +X017351Y013502D01* +X017851Y013002D01* +X019601Y013002D01* +X020101Y013502D01* +X020351Y013502D01* +X021851Y013502D01* +X022351Y013002D01* +X023601Y013002D01* +X024101Y013502D01* +X024351Y013502D01* +X025351Y013502D01* +X025851Y013002D01* +X027601Y013002D01* +X028101Y013502D01* +X028351Y013502D01* +X028101Y013002D02* +X029351Y013002D01* +X029851Y012502D01* +X029351Y012002D02* +X029851Y011502D01* +X029351Y012002D02* +X028101Y012002D01* +X027601Y011502D01* +X027351Y011502D01* +X027601Y012002D02* +X028101Y012502D01* +X028351Y012502D01* +X028101Y013002D02* +X027601Y012502D01* +X027351Y012502D01* +X027601Y012002D02* +X025851Y012002D01* +X025351Y012502D01* +X024351Y012502D01* +X024101Y012502D01* +X023601Y012002D01* +X021851Y012002D01* +X021351Y012502D01* +X020351Y012502D01* +X020101Y012502D01* +X019601Y012002D01* +X017851Y012002D01* +X017351Y012502D01* +X016351Y012502D01* +X016101Y012502D01* +X015601Y012002D01* +X013851Y012002D01* +X013351Y012502D01* +X012351Y012502D01* +X012101Y012502D01* +X011601Y012002D01* +X009851Y012002D01* +X009351Y012502D01* +X008351Y012502D01* +X008101Y012002D02* +X007851Y011752D01* +X007851Y008502D01* +X008351Y009502D02* +X009351Y009502D01* +X009851Y009002D01* +X011601Y009002D01* +X012101Y009502D01* +X012351Y009502D01* +X013351Y009502D01* +X013851Y009002D01* +X015601Y009002D01* +X016101Y009502D01* +X016351Y009502D01* +X017351Y009502D01* +X017851Y009002D01* +X019601Y009002D01* +X020101Y009502D01* +X020351Y009502D01* +X021351Y009502D01* +X021851Y009002D01* +X023601Y009002D01* +X024101Y009502D01* +X024351Y009502D01* +X025351Y009502D01* +X025851Y009002D01* +X027601Y009002D01* +X028101Y009502D01* +X028351Y009502D01* +X027601Y010002D02* +X028101Y010502D01* +X028351Y010502D01* +X028101Y011002D02* +X029351Y011002D01* +X029851Y010502D01* +X031351Y010502D01* +X031351Y009502D02* +X030351Y009502D01* +X028101Y011002D02* +X027601Y010502D01* +X027351Y010502D01* +X026101Y010502D01* +X025851Y010502D01* +X025351Y011002D01* +X024101Y011002D01* +X023601Y010502D01* +X023351Y010502D01* +X021851Y010502D01* +X021351Y011002D01* +X020101Y011002D01* +X019601Y010502D01* +X019351Y010502D01* +X017851Y010502D01* +X017351Y011002D01* +X016101Y011002D01* +X015601Y010502D01* +X015351Y010502D01* +X013851Y010502D01* +X013351Y011002D01* +X012101Y011002D01* +X011601Y010502D01* +X011351Y010502D01* +X011601Y010002D02* +X012101Y010502D01* +X012351Y010502D01* +X013351Y010502D01* +X013851Y010002D01* +X015601Y010002D01* +X016101Y010502D01* +X016351Y010502D01* +X017351Y010502D01* +X017851Y010002D01* +X019601Y010002D01* +X020101Y010502D01* +X020351Y010502D01* +X021351Y010502D01* +X021851Y010002D01* +X023601Y010002D01* +X024101Y010502D01* +X024351Y010502D01* +X025351Y010502D01* +X025851Y010002D01* +X027601Y010002D01* +X027601Y011002D02* +X028101Y011502D01* +X028351Y011502D01* +X027601Y011002D02* +X025851Y011002D01* +X025351Y011502D01* +X024351Y011502D01* +X024101Y011502D01* +X023601Y011002D01* +X021851Y011002D01* +X021351Y011502D01* +X020351Y011502D01* +X020101Y011502D01* +X019601Y011002D01* +X017851Y011002D01* +X017351Y011502D01* +X016351Y011502D01* +X016101Y011502D01* +X015601Y011002D01* +X013851Y011002D01* +X013351Y011502D01* +X012351Y011502D01* +X012101Y011502D01* +X011601Y011002D01* +X009851Y011002D01* +X009351Y011502D01* +X008351Y011502D01* +X008101Y012002D02* +X009351Y012002D01* +X009851Y011502D01* +X011351Y011502D01* +X011601Y013002D02* +X009851Y013002D01* +X009351Y013502D01* +X008351Y013502D01* +X008351Y010502D02* +X009351Y010502D01* +X009851Y010002D01* +X011601Y010002D01* +X014101Y011502D02* +X015351Y011502D01* +X015351Y012502D02* +X014101Y012502D01* +X018101Y012502D02* +X019351Y012502D01* +X019351Y011502D02* +X018101Y011502D01* +X021101Y012002D02* +X021351Y012002D01* +X021851Y011502D01* +X023351Y011502D01* +X023351Y012502D02* +X022101Y012502D01* +X021851Y012752D01* +X024101Y010002D02* +X025351Y010002D01* +X025851Y009502D01* +X026101Y009502D01* +X024101Y010002D02* +X023601Y009502D01* +X023351Y009502D01* +X024101Y007002D02* +X025351Y007002D01* +X025851Y006502D01* +X027351Y006502D01* +X027601Y006502D01* +X028101Y007002D01* +X029351Y007002D01* +X029851Y006502D01* +X031351Y006502D01* +X036351Y005502D02* +X036351Y008252D01* +X036351Y009252D01* +X036851Y008752D02* +X036851Y006002D01* +X037351Y005502D01* +X038351Y005502D02* +X038351Y008002D01* +X038101Y008252D01* +X037351Y008252D01* +X036851Y008752D02* +X037351Y009252D01* +X029101Y002252D02* +X021601Y002252D01* +X021351Y002502D01* +X021351Y004002D01* +X021851Y004502D01* +X021851Y006502D02* +X023351Y006502D01* +X023601Y006502D01* +X024101Y007002D01* +X021851Y006502D02* +X021351Y007002D01* +X020101Y007002D01* +X019601Y006502D01* +X019351Y006502D01* +X017851Y006502D01* +X017351Y007002D01* +X016101Y007002D01* +X015601Y006502D01* +X015351Y006502D01* +X013851Y006502D01* +X013351Y007002D01* +X012101Y007002D01* +X011601Y006502D01* +X011351Y006502D01* +X009351Y007502D02* +X008351Y007502D01* +X014351Y002002D02* +X014351Y001752D01* +X014101Y001502D01* +X013101Y001502D01* +X012851Y001752D01* +X034351Y001502D02* +X035351Y001502D01* +X035851Y002002D01* +X036851Y002002D01* +X037351Y001502D01* +X029851Y018502D02* +X029851Y019752D01* +X029351Y020252D01* +X029351Y025752D01* +X029101Y026002D01* +X019101Y026002D01* +X018101Y027002D01* +X011601Y027002D01* +X009351Y024752D01* +X005601Y024752D01* +X005351Y025002D01* +X005351Y028752D01* +X005101Y029002D01* +X002851Y029002D01* +X002851Y028002D02* +X004851Y028002D01* +X005101Y027752D01* +X005101Y024752D01* +X005351Y024502D01* +X015351Y024502D01* +X015601Y024252D01* +X025101Y024252D01* +X025351Y024002D01* +X025351Y020502D01* +X025851Y020002D01* +X027351Y020002D01* +X027851Y019502D01* +X027851Y018502D01* +X028351Y019752D02* +X028351Y023502D01* +X028351Y025502D01* +X028101Y025752D01* +X018851Y025752D01* +X018101Y026502D01* +X017351Y026502D01* +X012351Y026502D01* +X011351Y026002D02* +X015351Y026002D01* +X016351Y025002D01* +X026601Y025002D01* +X026851Y024752D01* +X026851Y022502D01* +X027351Y022002D02* +X027351Y025002D01* +X027101Y025252D01* +X017101Y025252D01* +X016851Y025502D01* +X016351Y026002D02* +X018101Y026002D01* +X018601Y025502D01* +X027601Y025502D01* +X027851Y025252D01* +X027851Y022502D01* +X027351Y022002D02* +X027851Y021502D01* +X026851Y021502D02* +X026351Y022002D01* +X026351Y024502D01* +X026101Y024752D01* +X016101Y024752D01* +X015351Y025502D01* +X013351Y025502D01* +X015351Y025002D02* +X015851Y024502D01* +X025601Y024502D01* +X025851Y024252D01* +X025851Y022502D01* +X024351Y023752D02* +X024101Y024002D01* +X015351Y024002D01* +X015101Y024252D01* +X005101Y024252D01* +X004851Y024502D01* +X004851Y026752D01* +X004601Y027002D01* +X002851Y027002D01* +X002851Y026002D02* +X004351Y026002D01* +X004601Y025752D01* +X004601Y024252D01* +X005101Y023752D01* +X005101Y021002D01* +X005351Y020752D01* +X022601Y020752D01* +X022851Y020502D01* +X022851Y018502D01* +X021351Y018502D02* +X021351Y020252D01* +X021101Y020502D01* +X005101Y020502D01* +X004851Y020752D01* +X004851Y023502D01* +X004351Y024002D01* +X004351Y024752D01* +X004101Y025002D01* +X002851Y025002D01* +X002851Y024002D02* +X003851Y024002D01* +X004601Y023252D01* +X004601Y020502D01* +X004851Y020252D01* +X019101Y020252D01* +X019351Y020002D01* +X019351Y018502D01* +X016351Y018502D02* +X016351Y019752D01* +X016101Y020002D01* +X004601Y020002D01* +X004351Y020252D01* +X004351Y022752D01* +X004101Y023002D01* +X002851Y023002D01* +X002851Y022002D02* +X003851Y022002D01* +X004101Y021752D01* +X004101Y020002D01* +X004351Y019752D01* +X014101Y019752D01* +X014351Y019502D01* +X014351Y018502D01* +X012851Y018502D02* +X012851Y019252D01* +X012601Y019502D01* +X004101Y019502D01* +X003851Y019752D01* +X003851Y020752D01* +X003601Y021002D01* +X002851Y021002D01* +X002851Y020002D02* +X003351Y020002D01* +X003601Y019752D01* +X003601Y019502D01* +X003851Y019252D01* +X010601Y019252D01* +X010851Y019002D01* +X010851Y018502D01* +X007851Y018502D02* +X007851Y018752D01* +X007601Y019002D01* +X002851Y019002D01* +X003601Y018002D02* +X004101Y018502D01* +X005851Y018502D01* +X003601Y018002D02* +X002851Y018002D01* +X006351Y023252D02* +X013351Y023252D01* +X013851Y023752D01* +X014601Y023752D01* +X014851Y023502D01* +X023101Y023502D01* +X023351Y023252D01* +X023351Y022002D01* +X023851Y021502D01* +X023851Y022502D02* +X023851Y023502D01* +X023601Y023752D01* +X015101Y023752D01* +X014851Y024002D01* +X009601Y024002D01* +X009351Y023752D01* +X010351Y025002D02* +X015351Y025002D01* +X014351Y023252D02* +X022601Y023252D01* +X022851Y023002D01* +X022851Y022502D01* +X024351Y023752D02* +X024351Y020502D01* +X024851Y020002D01* +X024851Y018502D01* +X018601Y027252D02* +X011351Y027252D01* +X009351Y025252D01* +X006351Y025252D01* +X006351Y026002D02* +X009601Y026002D01* +X011351Y027752D01* +X019101Y027752D01* +D18* +X019101Y027752D03* +X018601Y027252D03* +X017351Y026502D03* +X016851Y025502D03* +X016351Y026002D03* +X013351Y025502D03* +X012351Y026502D03* +X011351Y026002D03* +X010351Y025002D03* +X009351Y023752D03* +X006351Y023252D03* +X006351Y025252D03* +X006351Y026002D03* +X014351Y023252D03* +X021851Y013502D03* +X021851Y012752D03* +X021101Y012002D03* +X018101Y012502D03* +X018101Y011502D03* +X014101Y011502D03* +X014101Y012502D03* +X006601Y009002D03* +X006601Y008252D03* +X013351Y002002D03* +X014351Y002002D03* +X017351Y002502D03* +X018351Y002502D03* +X026101Y007002D03* +X026101Y008002D03* +X026101Y009502D03* +X026101Y010502D03* +X029851Y011502D03* +X029851Y012502D03* +X030351Y009502D03* +X033351Y002752D03* +X033851Y002252D03* +X031351Y002002D03* +X030351Y001502D03* +X029101Y002252D03* +X028351Y019752D03* +X028351Y023502D03* +D19* +X028851Y018502D02* +X028851Y017502D01* +X023851Y017502D01* +X023851Y018502D01* +X023851Y017502D02* +X020351Y017502D01* +X020351Y018502D01* +X020351Y017502D02* +X015351Y017502D01* +X015351Y018502D01* +X015351Y017502D02* +X011851Y017502D01* +X011851Y018502D01* +X011851Y017502D02* +X006851Y017502D01* +X004601Y017502D01* +X004601Y017002D01* +X004601Y012502D01* +X005601Y012502D01* +X005601Y013502D02* +X005601Y013752D01* +X007851Y015502D02* +X007851Y016502D01* +X016351Y016502D01* +X016351Y015502D01* +X017351Y015502D02* +X017351Y014502D01* +X018351Y014502D02* +X018351Y015502D01* +X019351Y014502D02* +X019351Y013502D01* +X023351Y013502D02* +X023351Y014502D01* +X024851Y015502D02* +X024851Y016502D01* +X031601Y016502D01* +X031601Y015752D01* +X032851Y015752D01* +X032851Y014752D02* +X032851Y014502D01* +X031351Y014502D02* +X031351Y013502D01* +X027351Y013502D02* +X027351Y014502D01* +X026851Y014502D02* +X026851Y015502D01* +X025851Y015502D02* +X025851Y014502D01* +X024851Y016502D02* +X016351Y016502D01* +X015351Y014502D02* +X015351Y013502D01* +X011351Y013502D02* +X011351Y014502D01* +X009851Y014502D02* +X009851Y015502D01* +X008851Y015502D02* +X008851Y014502D01* +X006851Y017502D02* +X006851Y018502D01* +X004601Y017002D02* +X002851Y017002D01* +X001851Y017002D01* +X001851Y018002D01* +X001851Y019002D01* +X001851Y020002D01* +X001851Y021002D01* +X001851Y022002D01* +X001851Y023002D01* +X001851Y024002D01* +X001851Y025002D01* +X001851Y026002D01* +X001851Y027002D01* +X001851Y028002D01* +X001851Y029002D01* +X001851Y011252D02* +X001851Y010002D01* +X002851Y010002D02* +X002851Y011252D01* +X002851Y009002D02* +X001851Y009002D01* +X002851Y009002D02* +X004101Y009002D01* +X004101Y006502D01* +X005601Y006502D01* +X005601Y006252D01* +X005601Y007502D02* +X005601Y007752D01* +X008351Y006502D02* +X008351Y005502D01* +X012351Y005502D02* +X012351Y006502D01* +X016351Y006502D02* +X016351Y005502D01* +X020351Y005502D02* +X020351Y006502D01* +X024351Y006502D02* +X024351Y005502D01* +X028351Y005502D02* +X028351Y006502D01* +X036351Y012252D02* +X036351Y012752D01* +X036351Y013252D01* +X037351Y013252D01* +X037351Y012252D01* +X036351Y012252D01* +X036351Y014252D02* +X036351Y015252D01* +X036851Y015252D01* +X037351Y015252D01* +X037351Y014252D01* +X036351Y014252D01* +X030851Y017502D02* +X028851Y017502D01* +X030851Y017502D02* +X030851Y018502D01* +X031851Y018502D01* +X036101Y021502D02* +X037101Y021502D01* +X037101Y020502D02* +X038101Y020502D01* +X038101Y025002D02* +X037101Y025002D01* +X037101Y026002D02* +X036101Y026002D01* +D20* +X035351Y026002D01* +X035351Y021502D01* +X036101Y021502D01* +X038101Y020502D02* +X038851Y020502D01* +X038851Y025002D01* +X038851Y031002D01* +X035101Y031002D01* +X006351Y031002D01* +X000601Y031002D01* +X000601Y011252D01* +X001851Y011252D01* +X002851Y011252D01* +X005601Y011252D01* +X005601Y010002D01* +X005601Y007752D01* +X005601Y006252D02* +X005601Y005502D01* +X008351Y005502D01* +X012351Y005502D01* +X016351Y005502D01* +X020351Y005502D01* +X024351Y005502D01* +X028351Y005502D01* +X032851Y005502D01* +X032851Y009502D01* +X032851Y012252D01* +X034101Y012252D01* +X034101Y015752D01* +X032851Y015752D01* +X032851Y017002D02* +X032851Y026252D01* +X031101Y028002D01* +X029291Y028002D01* +X029291Y029502D01* +X032241Y029502D02* +X034101Y027642D01* +X034101Y018252D01* +X036851Y018252D01* +X036851Y015252D01* +X035351Y017002D02* +X035351Y012752D01* +X036351Y012752D01* +X032851Y013502D02* +X032851Y014502D01* +X031351Y014502D01* +X027351Y014502D01* +X026851Y014502D01* +X025851Y014502D01* +X023351Y014502D01* +X019351Y014502D01* +X018351Y014502D01* +X017351Y014502D01* +X015351Y014502D01* +X011351Y014502D01* +X009851Y014502D01* +X008851Y014502D01* +X006851Y014502D01* +X006851Y013752D01* +X005601Y013752D01* +X006851Y013752D02* +X006851Y011252D01* +X005601Y011252D01* +X006351Y027502D02* +X006351Y031002D01* +X032851Y017002D02* +X035351Y017002D01* +X038101Y025002D02* +X038851Y025002D01* +X035191Y029502D02* +X035191Y031002D01* +X035101Y031002D01* +D21* +X036351Y015252D02* +X037351Y014252D01* +X036351Y014252D02* +X037351Y015252D01* +X037351Y013252D02* +X036351Y012252D01* +X036351Y013252D02* +X037351Y012252D01* +M02* diff --git a/testdata/hexapod.xln b/testdata/hexapod.xln new file mode 100644 index 0000000..4adb39e --- /dev/null +++ b/testdata/hexapod.xln @@ -0,0 +1,400 @@ +% +M48 +M72 +T01C0.0197 +T02C0.0320 +T03C0.0360 +T04C0.0400 +T05C0.0440 +T06C0.0520 +T07C0.1300 +% +T01 +X6601Y8252 +X6601Y9002 +X14101Y11502 +X14101Y12502 +X18101Y12502 +X18101Y11502 +X21101Y12002 +X21851Y12752 +X21851Y13502 +X26101Y10502 +X26101Y9502 +X26101Y8002 +X26101Y7002 +X30351Y9502 +X29851Y11502 +X29851Y12502 +X28351Y19752 +X28351Y23502 +X19101Y27752 +X18601Y27252 +X17351Y26502 +X16851Y25502 +X16351Y26002 +X13351Y25502 +X12351Y26502 +X11351Y26002 +X10351Y25002 +X9351Y23752 +X6351Y23252 +X6351Y25252 +X6351Y26002 +X14351Y23252 +X33351Y2752 +X33851Y2252 +X31351Y2002 +X30351Y1502 +X29101Y2252 +X18351Y2502 +X17351Y2502 +X14351Y2002 +X13351Y2002 +T02 +X12351Y6502 +X12351Y7502 +X12351Y8502 +X12351Y9502 +X12351Y10502 +X11351Y10502 +X11351Y9502 +X11351Y8502 +X11351Y7502 +X11351Y6502 +X8351Y6502 +X8351Y7502 +X8351Y8502 +X8351Y9502 +X8351Y10502 +X8351Y11502 +X8351Y12502 +X8351Y13502 +X8851Y15502 +X9851Y15502 +X10851Y15502 +X11851Y15502 +X12851Y15502 +X14351Y15502 +X15351Y15502 +X16351Y15502 +X17351Y15502 +X18351Y15502 +X19351Y15502 +X20351Y15502 +X21351Y15502 +X22851Y15502 +X23851Y15502 +X24851Y15502 +X25851Y15502 +X26851Y15502 +X27851Y15502 +X28851Y15502 +X29851Y15502 +X31351Y13502 +X31351Y12502 +X31351Y11502 +X31351Y10502 +X31351Y9502 +X31351Y8502 +X31351Y7502 +X31351Y6502 +X28351Y6502 +X28351Y7502 +X28351Y8502 +X28351Y9502 +X28351Y10502 +X27351Y10502 +X27351Y9502 +X27351Y8502 +X27351Y7502 +X27351Y6502 +X24351Y6502 +X23351Y6502 +X23351Y7502 +X24351Y7502 +X24351Y8502 +X23351Y8502 +X23351Y9502 +X24351Y9502 +X24351Y10502 +X23351Y10502 +X23351Y11502 +X24351Y11502 +X24351Y12502 +X23351Y12502 +X23351Y13502 +X24351Y13502 +X27351Y13502 +X28351Y13502 +X28351Y12502 +X28351Y11502 +X27351Y11502 +X27351Y12502 +X32851Y14752 +X32851Y15752 +X31851Y17502 +X31851Y18502 +X29851Y18502 +X28851Y18502 +X27851Y18502 +X26851Y18502 +X25851Y18502 +X24851Y18502 +X23851Y18502 +X22851Y18502 +X21351Y18502 +X20351Y18502 +X19351Y18502 +X18351Y18502 +X17351Y18502 +X16351Y18502 +X15351Y18502 +X14351Y18502 +X12851Y18502 +X11851Y18502 +X10851Y18502 +X9851Y18502 +X8851Y18502 +X7851Y18502 +X6851Y18502 +X5851Y18502 +X5851Y15502 +X6851Y15502 +X7851Y15502 +X5601Y13502 +X5601Y12502 +X11351Y12502 +X12351Y12502 +X12351Y13502 +X11351Y13502 +X11351Y11502 +X12351Y11502 +X15351Y11502 +X16351Y11502 +X16351Y12502 +X15351Y12502 +X15351Y13502 +X16351Y13502 +X19351Y13502 +X20351Y13502 +X20351Y12502 +X19351Y12502 +X19351Y11502 +X20351Y11502 +X20351Y10502 +X19351Y10502 +X19351Y9502 +X20351Y9502 +X20351Y8502 +X19351Y8502 +X19351Y7502 +X20351Y7502 +X20351Y6502 +X19351Y6502 +X16351Y6502 +X15351Y6502 +X15351Y7502 +X16351Y7502 +X16351Y8502 +X15351Y8502 +X15351Y9502 +X16351Y9502 +X16351Y10502 +X15351Y10502 +X5601Y7502 +X5601Y6502 +X30351Y24252 +X30351Y26252 +X34351Y5502 +X35351Y5502 +X36351Y5502 +X37351Y5502 +X38351Y5502 +X38351Y1502 +X37351Y1502 +X36351Y1502 +X35351Y1502 +X34351Y1502 +T03 +X1851Y3002 +X2851Y3002 +X2851Y4002 +X1851Y4002 +X1851Y5002 +X2851Y5002 +X2851Y6002 +X1851Y6002 +X1851Y7002 +X2851Y7002 +X2851Y8002 +X1851Y8002 +X1851Y9002 +X2851Y9002 +X2851Y10002 +X1851Y10002 +X1851Y17002 +X2851Y17002 +X2851Y18002 +X1851Y18002 +X1851Y19002 +X2851Y19002 +X2851Y20002 +X1851Y20002 +X1851Y21002 +X2851Y21002 +X2851Y22002 +X1851Y22002 +X1851Y23002 +X2851Y23002 +X2851Y24002 +X1851Y24002 +X1851Y25002 +X2851Y25002 +X2851Y26002 +X1851Y26002 +X1851Y27002 +X2851Y27002 +X2851Y28002 +X1851Y28002 +X1851Y29002 +X2851Y29002 +X36351Y15252 +X37351Y15252 +X37351Y14252 +X36351Y14252 +X36351Y13252 +X37351Y13252 +X37351Y12252 +X36351Y12252 +X36351Y11252 +X37351Y11252 +X37351Y10252 +X36351Y10252 +X36351Y9252 +X37351Y9252 +X37351Y8252 +X36351Y8252 +T04 +X31851Y4502 +X30851Y4502 +X29851Y4502 +X28851Y4502 +X27851Y4502 +X26851Y4502 +X25851Y4502 +X24851Y4502 +X23851Y4502 +X22851Y4502 +X21851Y4502 +X20851Y4502 +X19851Y4502 +X18851Y4502 +X17851Y4502 +X16851Y4502 +X15851Y4502 +X14851Y4502 +X13851Y4502 +X12851Y4502 +X11851Y4502 +X10851Y4502 +X9851Y4502 +X8851Y4502 +X7851Y4502 +X7851Y3502 +X8851Y3502 +X9851Y3502 +X10851Y3502 +X11851Y3502 +X12851Y3502 +X13851Y3502 +X14851Y3502 +X15851Y3502 +X16851Y3502 +X17851Y3502 +X18851Y3502 +X19851Y3502 +X20851Y3502 +X21851Y3502 +X22851Y3502 +X23851Y3502 +X24851Y3502 +X25851Y3502 +X26851Y3502 +X27851Y3502 +X28851Y3502 +X29851Y3502 +X30851Y3502 +X31851Y3502 +X31851Y21502 +X30851Y21502 +X29851Y21502 +X28851Y21502 +X27851Y21502 +X27851Y22502 +X28851Y22502 +X29851Y22502 +X30851Y22502 +X31851Y22502 +X26851Y22502 +X25851Y22502 +X24851Y22502 +X23851Y22502 +X22851Y22502 +X22851Y21502 +X23851Y21502 +X24851Y21502 +X25851Y21502 +X26851Y21502 +X21851Y21502 +X20851Y21502 +X19851Y21502 +X18851Y21502 +X17851Y21502 +X17851Y22502 +X18851Y22502 +X19851Y22502 +X20851Y22502 +X21851Y22502 +X16851Y22502 +X15851Y22502 +X14851Y22502 +X13851Y22502 +X12851Y22502 +X12851Y21502 +X13851Y21502 +X14851Y21502 +X15851Y21502 +X16851Y21502 +X11851Y21502 +X10851Y21502 +X9851Y21502 +X8851Y21502 +X7851Y21502 +X7851Y22502 +X8851Y22502 +X9851Y22502 +X10851Y22502 +X11851Y22502 +X21601Y28752 +X21601Y29752 +T05 +X20351Y27502 +X6351Y27502 +X32851Y13502 +X32851Y9502 +X37101Y19502 +X37101Y20502 +X37101Y21502 +X37101Y24002 +X37101Y25002 +X37101Y26002 +T06 +X35191Y29502 +X32241Y29502 +X29291Y29502 +X25351Y29502 +T07 +X44001Y25002 +X44001Y20502 +M30 diff --git a/testdata/layout1.cfg b/testdata/layout1.cfg new file mode 100644 index 0000000..d38bb0d --- /dev/null +++ b/testdata/layout1.cfg @@ -0,0 +1,272 @@ +# 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 = merge1 + +############################################################################# +# 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. +PanelWidth = 12.6 +PanelHeight = 7.8 + +# 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.1 +RightMargin = 0.1 +TopMargin = 0.1 +BottomMargin = 0.1 + +################################################################ +# +# 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.125 +YSpacing = 0.125 + +# 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.cmp +*BottomLayer=%(prefix)s.sol +*TopSilkscreen=%(prefix)s.plc +*BottomSilkscreen=%(prefix)s.pls +*TopSoldermask=%(prefix)s.stc +*BottomSoldermask=%(prefix)s.sts +Drills=%(prefix)s.xln +BoardOutline=%(prefix)s.bor +ToolList = toollist.%(prefix)s.drl +Placement = placement.%(prefix)s.txt + +############################################################################## +# 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/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.cmp +*BottomLayer=%(prefix)s.sol +*TopSilkscreen=%(prefix)s.plc +*BottomSilkscreen=%(prefix)s.pls +*TopSoldermask=%(prefix)s.stc +*BottomSoldermask=%(prefix)s.sts +Drills=%(prefix)s.xln +BoardOutline=%(prefix)s.bor + +# 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 diff --git a/testdata/layout1.def b/testdata/layout1.def new file mode 100644 index 0000000..120ea57 --- /dev/null +++ b/testdata/layout1.def @@ -0,0 +1,19 @@ +# This example simply takes the small Proj1 board and panelizes +# it in a 2x4 array. To demonstrate rotation, the second column +# consists of rotated jobs. You wouldn't really do it this way, +# of course, as it wastes space. +Row { + Col { + Proj1 + Proj1 + Proj1 + Proj1 + } + Col { + Proj1 Rotate90 + Proj1 Rotate180 + Proj1 Rotate270 + Proj1 Rotate + } +} + diff --git a/testdata/layout2.cfg b/testdata/layout2.cfg new file mode 100644 index 0000000..2eb980e --- /dev/null +++ b/testdata/layout2.cfg @@ -0,0 +1,282 @@ +# This configuration file demonstrates panelizing multiple, different jobs. +# We panelize the HEXAPOD job and several copies of the Proj1 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 = merge2 + +############################################################################# +# 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. +PanelWidth = 12.6 +PanelHeight = 7.8 + +# 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 +# an OutlineLayer. +LeftMargin = 0.1 +RightMargin = 0.1 +TopMargin = 0.1 +BottomMargin = 0.1 + +################################################################ +# +# 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.125 +YSpacing = 0.125 + +# 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 = 1 + +# 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.cmp +*BottomLayer=%(prefix)s.sol +*TopSilkscreen=%(prefix)s.plc +*BottomSilkscreen=%(prefix)s.pls +*TopSoldermask=%(prefix)s.stc +*BottomSoldermask=%(prefix)s.sts +Drills=%(prefix)s.xln +BoardOutline=%(prefix)s.bor +ToolList = toollist.%(prefix)s.drl +Placement = placement.%(prefix)s.txt + +############################################################################## +# 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/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.cmp +*BottomLayer=%(prefix)s.sol +*TopSilkscreen=%(prefix)s.plc +*BottomSilkscreen=%(prefix)s.pls +*TopSoldermask=%(prefix)s.stc +*BottomSoldermask=%(prefix)s.sts +Drills=%(prefix)s.xln +BoardOutline=%(prefix)s.bor + +# 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 = 11 + +[Hexapod] +Prefix=%(projdir)s/hexapod +*TopLayer=%(prefix)s.cmp +*BottomLayer=%(prefix)s.sol +*TopSilkscreen=%(prefix)s.plc +Drills=%(prefix)s.xln +BoardOutline=%(prefix)s.bor diff --git a/testdata/layout2.def b/testdata/layout2.def new file mode 100644 index 0000000..251cba5 --- /dev/null +++ b/testdata/layout2.def @@ -0,0 +1,57 @@ +# This layout merges a Hexapod and Proj1 boards into a single +# panel. The layout demonstrates nested rows and columns. The +# final arrangement looks like this (make sure you are looking +# at this document with a fixed-width font like Courier): +# +# +-----------------------------------------------------+ +# | Proj1 | Proj1 | Proj1 | Proj1 | Proj1 | | +# | | | | | | | +# | | | | | | | +# +---------+---------+---------+---------+--------+ | +# | | +# | +-------+-------+ +# | | P | P | +# +--------------------------------+ | r | r | +# | | | o | o | +# | | | j | j | +# | | | 1 | 1 | +# | | +-------+-------+ +# | | | P | P | +# | | | r | r | +# | | | o | o | +# | Hexapod | | j | j | +# | | | 1 | 1 | +# | | +-------+-------+ +# | | | P | P | +# | | | r | r | +# | | | o | o | +# | | | j | j | +# | | | 1 | 1 | +# +--------------------------------+----+-------+-------+ + +Row { // First row has the hexapod and 2x3 panel of + // rotated Proj1 jobs. + Hexapod + Col { // Could also write this as two separate 1x3 columns + Row { // First 1x2 row + Proj1 Rotate + Proj1 Rotate + } + Row { // Second 1x2 row above first one + Proj1 Rotate + Proj1 Rotate + } + Row { // Third 1x2 row above second row + Proj1 Rotate + Proj1 Rotate + } + } // end of column +} // end of first row + +Row { // Second row has 5x1 panel of Proj1 + Proj1 + Proj1 + Proj1 + Proj1 + Proj1 +} diff --git a/testdata/proj1.bor b/testdata/proj1.bor new file mode 100644 index 0000000..0b33046 --- /dev/null +++ b/testdata/proj1.bor @@ -0,0 +1,52 @@ +G75* +G70* +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0000*% +D10* +X000740Y000477D02* +X000740Y009873D01* +X011210Y009873D01* +X011210Y000477D01* +X000740Y000477D01* +X005505Y005625D02* +X005507Y005647D01* +X005513Y005668D01* +X005523Y005688D01* +X005536Y005706D01* +X005553Y005721D01* +X005572Y005732D01* +X005592Y005740D01* +X005614Y005744D01* +X005636Y005744D01* +X005658Y005740D01* +X005678Y005732D01* +X005697Y005721D01* +X005714Y005706D01* +X005727Y005688D01* +X005737Y005668D01* +X005743Y005647D01* +X005745Y005625D01* +X005743Y005603D01* +X005737Y005582D01* +X005727Y005562D01* +X005714Y005544D01* +X005697Y005529D01* +X005678Y005518D01* +X005658Y005510D01* +X005636Y005506D01* +X005614Y005506D01* +X005592Y005510D01* +X005572Y005518D01* +X005553Y005529D01* +X005536Y005544D01* +X005523Y005562D01* +X005513Y005582D01* +X005507Y005603D01* +X005505Y005625D01* +M02* diff --git a/testdata/proj1.brd b/testdata/proj1.brd new file mode 100644 index 0000000..66a2a30 Binary files /dev/null and b/testdata/proj1.brd differ diff --git a/testdata/proj1.cam b/testdata/proj1.cam new file mode 100644 index 0000000..f0481ae --- /dev/null +++ b/testdata/proj1.cam @@ -0,0 +1,153 @@ +[CAM Processor Job] +Description="" +Section=Sec_1 +Section=Sec_2 +Section=Sec_3 +Section=Sec_4 +Section=Sec_5 +Section=Sec_6 +Section=Sec_7 +Section=Sec_8 + +[Sec_1] +Name="TopLayer" +Prompt="" +Device="GERBER_RS274X" +Wheel="" +Rack="" +Scale=1.000000 +Output="proj1.cmp" +Flags="0 0 0 1 0 1 1" +Emulate="0 0 0" +Offset="0.0mil 0.0mil" +Sheet=1 +Tolerance="0.000000 0.000000 0.000000 0.000000 0.000000 0.000000" +Pen="0.0mil 0.000000" +Page="12000.0mil 8000.0mil" +Layers=" 1 17 18 50 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 252 253 255" +Colors=" 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 6 6 4 8 8 8 8 8 8 8 8 8 8 8 8 8 4 4 1 1 1 1 3 3 1 2 6 8 8 5 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 2 4 3 6 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0" + +[Sec_2] +Name="BottomLayer" +Prompt="" +Device="GERBER_RS274X" +Wheel="" +Rack="" +Scale=1.000000 +Output="proj1.sol" +Flags="0 0 0 1 0 1 1" +Emulate="0 0 0" +Offset="0.0mil 0.0mil" +Sheet=1 +Tolerance="0.000000 0.000000 0.000000 0.000000 0.000000 0.000000" +Pen="0.0mil 0.000000" +Page="12000.0mil 8000.0mil" +Layers=" 16 17 18 50 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 252 253 255" +Colors=" 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 6 6 4 8 8 8 8 8 8 8 8 8 8 8 8 8 4 4 1 1 1 1 3 3 1 2 6 8 8 5 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 2 4 3 6 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0" + +[Sec_3] +Name="BoardOutline" +Prompt="" +Device="GERBER_RS274X" +Wheel="" +Rack="" +Scale=1.000000 +Output="proj1.bor" +Flags="0 0 0 1 0 1 1" +Emulate="0 0 0" +Offset="0.0mil 0.0mil" +Sheet=1 +Tolerance="0.000000 0.000000 0.000000 0.000000 0.000000 0.000000" +Pen="0.0mil 0.000000" +Page="12000.0mil 8000.0mil" +Layers=" 20 50 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 252 253 255" +Colors=" 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 6 6 4 8 8 8 8 8 8 8 8 8 8 8 8 8 4 4 1 1 1 1 3 3 1 2 6 8 8 5 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 2 4 3 6 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0" + +[Sec_4] +Name="Drills" +Prompt="" +Device="EXCELLON" +Wheel="" +Scale=1.000000 +Output="proj1.xln" +Flags="0 0 0 1 0 1 1" +Emulate="0 0 0" +Offset="0.0mil 0.0mil" +Sheet=1 +Tolerance="0.000000 0.000000 0.000000 0.000000 0.020000 0.100000" +Pen="0.0mil 0.000000" +Page="12000.0mil 8000.0mil" +Layers=" 44 45 50 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 252 253 255" +Colors=" 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 6 6 4 8 8 8 8 8 8 8 8 8 8 8 8 8 4 4 1 1 1 1 3 3 1 2 6 8 8 5 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 2 4 3 6 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0" + +[Sec_5] +Name="Top Soldermask" +Prompt="" +Device="GERBER_RS274X" +Wheel="" +Rack="proj1.drl" +Scale=1.000000 +Output="proj1.stc" +Flags="0 0 0 1 0 1 1" +Emulate="0 0 0" +Offset="0.0mil 0.0mil" +Sheet=1 +Tolerance="0.000000 0.000000 0.000000 0.000000 0.020000 0.100000" +Pen="0.0mil 0.000000" +Page="12000.0mil 8000.0mil" +Layers=" 29 50 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 252 253 255" +Colors=" 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 6 6 4 8 8 8 8 8 8 8 8 8 8 8 8 8 4 4 1 1 1 1 3 3 1 2 6 8 8 5 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 2 4 3 6 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0" + +[Sec_6] +Name="Bottom Soldermask" +Prompt="" +Device="GERBER_RS274X" +Wheel="" +Rack="proj1.drl" +Scale=1.000000 +Output="proj1.sts" +Flags="0 0 0 1 0 1 1" +Emulate="0 0 0" +Offset="0.0mil 0.0mil" +Sheet=1 +Tolerance="0.000000 0.000000 0.000000 0.000000 0.020000 0.100000" +Pen="0.0mil 0.000000" +Page="12000.0mil 8000.0mil" +Layers=" 30 50 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 252 253 255" +Colors=" 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 6 6 4 8 8 8 8 8 8 8 8 8 8 8 8 8 4 4 1 1 1 1 3 3 1 2 6 8 8 5 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 2 4 3 6 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0" + +[Sec_7] +Name="Top Silkscreen" +Prompt="" +Device="GERBER_RS274X" +Wheel="" +Rack="proj1.drl" +Scale=1.000000 +Output="proj1.plc" +Flags="0 0 0 1 0 1 1" +Emulate="0 0 0" +Offset="0.0mil 0.0mil" +Sheet=1 +Tolerance="0.000000 0.000000 0.000000 0.000000 0.020000 0.100000" +Pen="0.0mil 0.000000" +Page="12000.0mil 8000.0mil" +Layers=" 21 25 50 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 252 253 255" +Colors=" 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 6 6 4 8 8 8 8 8 8 8 8 8 8 8 8 8 4 4 1 1 1 1 3 3 1 2 6 8 8 5 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 2 4 3 6 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0" + +[Sec_8] +Name="Bottom Silkscreen" +Prompt="" +Device="GERBER_RS274X" +Wheel="" +Rack="proj1.drl" +Scale=1.000000 +Output="proj1.pls" +Flags="0 0 0 1 0 1 1" +Emulate="0 0 0" +Offset="0.0mil 0.0mil" +Sheet=1 +Tolerance="0.000000 0.000000 0.000000 0.000000 0.020000 0.100000" +Pen="0.0mil 0.000000" +Page="12000.0mil 8000.0mil" +Layers=" 22 26 50 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 252 253 255" +Colors=" 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 6 6 4 8 8 8 8 8 8 8 8 8 8 8 8 8 4 4 1 1 1 1 3 3 1 2 6 8 8 5 8 8 8 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 4 2 4 3 6 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0" diff --git a/testdata/proj1.cmp b/testdata/proj1.cmp new file mode 100644 index 0000000..8ae1cd0 --- /dev/null +++ b/testdata/proj1.cmp @@ -0,0 +1,765 @@ +G75* +G70* +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0060*% +%ADD11R,0.1500X0.1000*% +%ADD12C,0.0160*% +%ADD13OC8,0.0850*% +%ADD14O,0.0780X0.1560*% +%ADD15O,0.1560X0.0780*% +%ADD16O,0.1200X0.0600*% +%ADD17C,0.0400*% +%ADD18R,0.0400X0.0400*% +%ADD19C,0.0020*% +D10* +X004768Y008755D02* +X004768Y009395D01* +X004555Y009395D02* +X004982Y009395D01* +X005199Y009075D02* +X005306Y009182D01* +X005520Y009182D01* +X005626Y009075D01* +X005626Y008968D01* +X005199Y008968D01* +X005199Y008862D02* +X005199Y009075D01* +X005199Y008862D02* +X005306Y008755D01* +X005520Y008755D01* +X005844Y008755D02* +X006271Y009182D01* +X006488Y009182D02* +X006702Y009182D01* +X006595Y009289D02* +X006595Y008862D01* +X006702Y008755D01* +X006271Y008755D02* +X005844Y009182D01* +D11* +X002375Y009125D03* +D12* +X007225Y006925D02* +X009725Y006925D01* +X009725Y004925D01* +X007225Y004925D01* +X007225Y006925D01* +X007225Y006808D02* +X009725Y006808D01* +X009725Y006650D02* +X007225Y006650D01* +X007225Y006491D02* +X007935Y006491D01* +X007974Y006530D02* +X007620Y006175D01* +X007620Y005975D01* +X008175Y005975D01* +X008175Y006530D01* +X007974Y006530D01* +X008175Y006491D02* +X008275Y006491D01* +X008275Y006530D02* +X008475Y006530D01* +X008830Y006175D01* +X008830Y005975D01* +X008275Y005975D01* +X008275Y005875D01* +X008830Y005875D01* +X008830Y005674D01* +X008475Y005320D01* +X008275Y005320D01* +X008275Y005875D01* +X008175Y005875D01* +X008175Y005320D01* +X007974Y005320D01* +X007620Y005674D01* +X007620Y005875D01* +X008175Y005875D01* +X008175Y005975D01* +X008275Y005975D01* +X008275Y006530D01* +X008275Y006332D02* +X008175Y006332D01* +X008175Y006174D02* +X008275Y006174D01* +X008275Y006015D02* +X008175Y006015D01* +X008175Y005857D02* +X008275Y005857D01* +X008275Y005698D02* +X008175Y005698D01* +X008175Y005540D02* +X008275Y005540D01* +X008275Y005381D02* +X008175Y005381D01* +X007913Y005381D02* +X007225Y005381D01* +X007225Y005223D02* +X009725Y005223D01* +X009725Y005381D02* +X008537Y005381D01* +X008695Y005540D02* +X009725Y005540D01* +X009725Y005698D02* +X008830Y005698D01* +X008830Y005857D02* +X009725Y005857D01* +X009725Y006015D02* +X008830Y006015D01* +X008830Y006174D02* +X009725Y006174D01* +X009725Y006332D02* +X008673Y006332D01* +X008514Y006491D02* +X009725Y006491D01* +X009765Y007845D02* +X009625Y008125D01* +X009625Y008625D01* +X010009Y009393D01* +X010241Y009393D01* +X010625Y008625D01* +X010625Y008125D01* +X010485Y007845D01* +X010485Y008291D01* +X010407Y008369D01* +X010447Y008410D01* +X010505Y008549D01* +X010505Y008625D01* +X010505Y008700D01* +X010447Y008840D01* +X010340Y008947D01* +X010200Y009005D01* +X010125Y009005D01* +X010125Y008625D01* +X010125Y008625D01* +X010505Y008625D01* +X010125Y008625D01* +X010125Y008625D01* +X010125Y009005D01* +X010049Y009005D01* +X009910Y008947D01* +X009803Y008840D01* +X009745Y008700D01* +X009745Y008625D01* +X010124Y008625D01* +X010124Y008625D01* +X009745Y008625D01* +X009745Y008549D01* +X009803Y008410D01* +X009843Y008369D01* +X009765Y008291D01* +X009765Y007845D01* +X009765Y007918D02* +X009728Y007918D01* +X009765Y008076D02* +X009649Y008076D01* +X009625Y008235D02* +X009765Y008235D01* +X009819Y008394D02* +X009625Y008394D01* +X009625Y008552D02* +X009745Y008552D01* +X009749Y008711D02* +X009668Y008711D01* +X009747Y008869D02* +X009832Y008869D01* +X009826Y009028D02* +X010423Y009028D01* +X010418Y008869D02* +X010503Y008869D01* +X010501Y008711D02* +X010582Y008711D01* +X010625Y008552D02* +X010505Y008552D01* +X010431Y008394D02* +X010625Y008394D01* +X010625Y008235D02* +X010485Y008235D01* +X010485Y008076D02* +X010601Y008076D01* +X010521Y007918D02* +X010485Y007918D01* +X010125Y008711D02* +X010125Y008711D01* +X010125Y008869D02* +X010125Y008869D01* +X009906Y009186D02* +X010344Y009186D01* +X010265Y009345D02* +X009985Y009345D01* +X007777Y006332D02* +X007225Y006332D01* +X007225Y006174D02* +X007620Y006174D01* +X007620Y006015D02* +X007225Y006015D01* +X007225Y005857D02* +X007620Y005857D01* +X007620Y005698D02* +X007225Y005698D01* +X007225Y005540D02* +X007754Y005540D01* +X007225Y005064D02* +X009725Y005064D01* +X006625Y004525D02* +X006625Y003525D01* +X004625Y003525D01* +X004918Y005625D02* +X004921Y005689D01* +X004930Y005753D01* +X004944Y005816D01* +X004964Y005877D01* +X004990Y005936D01* +X005021Y005992D01* +X005057Y006046D01* +X005097Y006096D01* +X005142Y006142D01* +X005191Y006183D01* +X005244Y006221D01* +X005300Y006253D01* +X005358Y006280D01* +X005419Y006301D01* +X005481Y006317D01* +X005545Y006327D01* +X005609Y006332D01* +X005673Y006330D01* +X005737Y006323D01* +X005800Y006310D01* +X005862Y006291D01* +X005921Y006267D01* +X005978Y006237D01* +X006033Y006203D01* +X006084Y006163D01* +X006131Y006119D01* +X006173Y006071D01* +X006212Y006019D01* +X006245Y005964D01* +X006273Y005907D01* +X006296Y005847D01* +X006314Y005785D01* +X006325Y005721D01* +X006331Y005657D01* +X006331Y005593D01* +X006325Y005529D01* +X006314Y005465D01* +X006296Y005403D01* +X006273Y005343D01* +X006245Y005286D01* +X006212Y005231D01* +X006173Y005179D01* +X006131Y005131D01* +X006084Y005087D01* +X006033Y005047D01* +X005979Y005013D01* +X005921Y004983D01* +X005862Y004959D01* +X005800Y004940D01* +X005737Y004927D01* +X005673Y004920D01* +X005609Y004918D01* +X005545Y004923D01* +X005481Y004933D01* +X005419Y004949D01* +X005358Y004970D01* +X005300Y004997D01* +X005244Y005029D01* +X005191Y005067D01* +X005142Y005108D01* +X005097Y005154D01* +X005057Y005204D01* +X005021Y005258D01* +X004990Y005314D01* +X004964Y005373D01* +X004944Y005434D01* +X004930Y005497D01* +X004921Y005561D01* +X004918Y005625D01* +D13* +X001625Y005625D03* +X008225Y005925D03* +D14* +X001625Y001625D03* +D15* +X003625Y001625D03* +D16* +X009564Y002233D03* +X009564Y003217D03* +D17* +X010125Y008625D03* +D18* +X010125Y008025D03* +D19* +X005634Y001056D02* +X005634Y001196D01* +X005635Y001195D02* +X005682Y001198D01* +X005728Y001205D01* +X005773Y001215D01* +X005817Y001229D01* +X005860Y001247D01* +X005902Y001269D01* +X005941Y001294D01* +X005978Y001322D01* +X006013Y001353D01* +X006045Y001387D01* +X006074Y001424D01* +X006100Y001462D01* +X006122Y001503D01* +X006141Y001546D01* +X006156Y001590D01* +X006168Y001635D01* +X006176Y001681D01* +X006180Y001728D01* +X006179Y001774D01* +X006175Y001821D01* +X006168Y001867D01* +X006156Y001912D01* +X006140Y001956D01* +X006121Y001998D01* +X006099Y002039D01* +X006073Y002078D01* +X006044Y002114D01* +X006012Y002148D01* +X005977Y002179D01* +X005940Y002207D01* +X005900Y002232D01* +X005859Y002253D01* +X005816Y002271D01* +X005771Y002285D01* +X005726Y002296D01* +X005680Y002302D01* +X005633Y002305D01* +X005587Y002304D01* +X005540Y002299D01* +X005495Y002289D01* +X005450Y002277D01* +X005406Y002260D01* +X005364Y002240D01* +X005324Y002216D01* +X005286Y002189D01* +X005250Y002159D01* +X005217Y002126D01* +X005187Y002091D01* +X005076Y002174D01* +X005075Y002175D01* +X005109Y002216D01* +X005146Y002254D01* +X005186Y002289D01* +X005228Y002320D01* +X005273Y002349D01* +X005319Y002374D01* +X005368Y002396D01* +X005418Y002413D01* +X005469Y002427D01* +X005521Y002437D01* +X005573Y002443D01* +X005626Y002445D01* +X005679Y002443D01* +X005732Y002437D01* +X005784Y002427D01* +X005835Y002413D01* +X005885Y002395D01* +X005933Y002373D01* +X005980Y002348D01* +X006024Y002319D01* +X006066Y002287D01* +X006106Y002252D01* +X006143Y002214D01* +X006176Y002173D01* +X006207Y002130D01* +X006234Y002084D01* +X006258Y002037D01* +X006278Y001988D01* +X006294Y001937D01* +X006307Y001886D01* +X006315Y001834D01* +X006319Y001781D01* +X006320Y001728D01* +X006316Y001675D01* +X006308Y001623D01* +X006297Y001571D01* +X006281Y001520D01* +X006262Y001471D01* +X006238Y001423D01* +X006212Y001378D01* +X006182Y001334D01* +X006148Y001293D01* +X006112Y001254D01* +X006073Y001219D01* +X006031Y001186D01* +X005987Y001157D01* +X005941Y001131D01* +X005893Y001109D01* +X005843Y001090D01* +X005792Y001075D01* +X005740Y001065D01* +X005688Y001058D01* +X005635Y001055D01* +X005635Y001073D01* +X005687Y001076D01* +X005738Y001082D01* +X005788Y001093D01* +X005838Y001107D01* +X005886Y001125D01* +X005933Y001147D01* +X005978Y001172D01* +X006021Y001201D01* +X006061Y001233D01* +X006100Y001267D01* +X006135Y001305D01* +X006167Y001345D01* +X006197Y001387D01* +X006223Y001432D01* +X006245Y001478D01* +X006264Y001526D01* +X006279Y001576D01* +X006291Y001626D01* +X006298Y001677D01* +X006302Y001729D01* +X006301Y001780D01* +X006297Y001832D01* +X006289Y001882D01* +X006277Y001933D01* +X006261Y001982D01* +X006242Y002029D01* +X006219Y002076D01* +X006192Y002120D01* +X006162Y002162D01* +X006129Y002202D01* +X006093Y002239D01* +X006055Y002273D01* +X006014Y002304D01* +X005970Y002332D01* +X005925Y002357D01* +X005878Y002378D01* +X005830Y002395D01* +X005780Y002409D01* +X005729Y002419D01* +X005678Y002425D01* +X005626Y002427D01* +X005575Y002425D01* +X005524Y002419D01* +X005473Y002410D01* +X005423Y002396D01* +X005374Y002379D01* +X005327Y002358D01* +X005282Y002334D01* +X005238Y002306D01* +X005197Y002275D01* +X005159Y002241D01* +X005123Y002204D01* +X005089Y002164D01* +X005104Y002153D01* +X005137Y002193D01* +X005173Y002229D01* +X005211Y002263D01* +X005252Y002293D01* +X005295Y002321D01* +X005341Y002344D01* +X005388Y002365D01* +X005436Y002381D01* +X005486Y002394D01* +X005536Y002403D01* +X005587Y002408D01* +X005638Y002409D01* +X005689Y002406D01* +X005740Y002399D01* +X005790Y002388D01* +X005839Y002373D01* +X005887Y002355D01* +X005933Y002333D01* +X005977Y002307D01* +X006019Y002278D01* +X006059Y002246D01* +X006096Y002211D01* +X006130Y002173D01* +X006162Y002132D01* +X006190Y002090D01* +X006214Y002045D01* +X006236Y001998D01* +X006253Y001950D01* +X006267Y001901D01* +X006276Y001850D01* +X006282Y001800D01* +X006284Y001748D01* +X006282Y001697D01* +X006276Y001647D01* +X006266Y001596D01* +X006252Y001547D01* +X006234Y001499D01* +X006213Y001453D01* +X006188Y001408D01* +X006160Y001365D01* +X006128Y001325D01* +X006094Y001287D01* +X006057Y001252D01* +X006017Y001220D01* +X005974Y001191D01* +X005930Y001166D01* +X005884Y001144D01* +X005836Y001126D01* +X005787Y001111D01* +X005737Y001101D01* +X005686Y001094D01* +X005635Y001091D01* +X005635Y001109D01* +X005686Y001112D01* +X005736Y001119D01* +X005786Y001129D01* +X005834Y001144D01* +X005881Y001162D01* +X005927Y001185D01* +X005971Y001210D01* +X006012Y001239D01* +X006052Y001272D01* +X006088Y001307D01* +X006122Y001345D01* +X006152Y001385D01* +X006179Y001428D01* +X006203Y001473D01* +X006223Y001520D01* +X006240Y001568D01* +X006252Y001617D01* +X006261Y001667D01* +X006265Y001717D01* +X006266Y001768D01* +X006262Y001819D01* +X006255Y001869D01* +X006244Y001918D01* +X006228Y001967D01* +X006209Y002014D01* +X006187Y002059D01* +X006160Y002103D01* +X006131Y002144D01* +X006098Y002183D01* +X006062Y002219D01* +X006024Y002252D01* +X005983Y002282D01* +X005940Y002308D01* +X005895Y002332D01* +X005848Y002351D01* +X005800Y002367D01* +X005750Y002379D01* +X005700Y002387D01* +X005650Y002391D01* +X005599Y002390D01* +X005548Y002386D01* +X005498Y002378D01* +X005449Y002366D01* +X005401Y002350D01* +X005354Y002331D01* +X005309Y002308D01* +X005266Y002281D01* +X005225Y002251D01* +X005186Y002218D01* +X005151Y002181D01* +X005118Y002143D01* +X005133Y002132D01* +X005164Y002169D01* +X005199Y002205D01* +X005236Y002237D01* +X005276Y002266D01* +X005318Y002292D01* +X005362Y002315D01* +X005407Y002334D01* +X005454Y002349D01* +X005502Y002361D01* +X005551Y002369D01* +X005600Y002372D01* +X005649Y002373D01* +X005698Y002369D01* +X005747Y002361D01* +X005795Y002349D01* +X005842Y002334D01* +X005887Y002315D01* +X005931Y002293D01* +X005973Y002267D01* +X006013Y002238D01* +X006050Y002205D01* +X006085Y002170D01* +X006117Y002133D01* +X006145Y002093D01* +X006171Y002050D01* +X006193Y002006D01* +X006211Y001961D01* +X006226Y001914D01* +X006237Y001866D01* +X006244Y001817D01* +X006248Y001768D01* +X006247Y001718D01* +X006243Y001669D01* +X006234Y001621D01* +X006222Y001573D01* +X006206Y001526D01* +X006187Y001481D01* +X006164Y001437D01* +X006137Y001396D01* +X006108Y001356D01* +X006075Y001319D01* +X006040Y001285D01* +X006002Y001254D01* +X005961Y001226D01* +X005919Y001201D01* +X005874Y001179D01* +X005828Y001161D01* +X005781Y001147D01* +X005733Y001136D01* +X005684Y001130D01* +X005635Y001127D01* +X005635Y001145D01* +X005684Y001148D01* +X005732Y001155D01* +X005780Y001165D01* +X005826Y001180D01* +X005872Y001198D01* +X005915Y001219D01* +X005957Y001244D01* +X005997Y001273D01* +X006034Y001304D01* +X006069Y001339D01* +X006101Y001376D01* +X006129Y001416D01* +X006154Y001457D01* +X006176Y001501D01* +X006195Y001546D01* +X006209Y001593D01* +X006220Y001640D01* +X006227Y001689D01* +X006230Y001738D01* +X006229Y001786D01* +X006224Y001835D01* +X006215Y001883D01* +X006203Y001930D01* +X006186Y001976D01* +X006166Y002021D01* +X006143Y002063D01* +X006116Y002104D01* +X006085Y002142D01* +X006052Y002178D01* +X006016Y002211D01* +X005978Y002241D01* +X005937Y002268D01* +X005894Y002292D01* +X005850Y002312D01* +X005804Y002328D01* +X005757Y002341D01* +X005709Y002349D01* +X005660Y002354D01* +X005611Y002355D01* +X005562Y002352D01* +X005514Y002345D01* +X005467Y002334D01* +X005420Y002319D01* +X005375Y002301D01* +X005331Y002279D01* +X005289Y002253D01* +X005250Y002225D01* +X005213Y002193D01* +X005179Y002158D01* +X005147Y002121D01* +X005161Y002110D01* +X005193Y002147D01* +X005227Y002181D01* +X005264Y002213D01* +X005303Y002241D01* +X005344Y002266D01* +X005388Y002287D01* +X005433Y002305D01* +X005479Y002319D01* +X005526Y002329D01* +X005574Y002335D01* +X005622Y002337D01* +X005671Y002335D01* +X005719Y002329D01* +X005766Y002320D01* +X005812Y002306D01* +X005857Y002289D01* +X005901Y002268D01* +X005943Y002244D01* +X005982Y002216D01* +X006019Y002185D01* +X006054Y002151D01* +X006085Y002114D01* +X006114Y002075D01* +X006139Y002034D01* +X006160Y001991D01* +X006178Y001946D01* +X006193Y001900D01* +X006203Y001852D01* +X006209Y001805D01* +X006212Y001756D01* +X006210Y001708D01* +X006205Y001660D01* +X006196Y001613D01* +X006182Y001566D01* +X006165Y001521D01* +X006145Y001477D01* +X006121Y001435D01* +X006093Y001396D01* +X006062Y001358D01* +X006029Y001324D01* +X005992Y001292D01* +X005953Y001263D01* +X005912Y001238D01* +X005869Y001216D01* +X005824Y001198D01* +X005778Y001183D01* +X005731Y001173D01* +X005683Y001166D01* +X005635Y001163D01* +X005635Y001181D01* +X005683Y001184D01* +X005730Y001191D01* +X005777Y001202D01* +X005822Y001216D01* +X005866Y001235D01* +X005909Y001257D01* +X005949Y001282D01* +X005987Y001311D01* +X006023Y001343D01* +X006055Y001378D01* +X006085Y001415D01* +X006112Y001455D01* +X006135Y001497D01* +X006154Y001541D01* +X006170Y001586D01* +X006182Y001632D01* +X006190Y001679D01* +X006194Y001727D01* +X006193Y001775D01* +X006189Y001822D01* +X006181Y001869D01* +X006169Y001916D01* +X006153Y001961D01* +X006134Y002004D01* +X006111Y002046D01* +X006084Y002086D01* +X006054Y002123D01* +X006022Y002158D01* +X005986Y002190D01* +X005948Y002219D01* +X005907Y002244D01* +X005865Y002266D01* +X005821Y002284D01* +X005775Y002299D01* +X005728Y002310D01* +X005681Y002316D01* +X005633Y002319D01* +X005586Y002318D01* +X005538Y002312D01* +X005491Y002303D01* +X005445Y002290D01* +X005401Y002273D01* +X005357Y002252D01* +X005316Y002228D01* +X005277Y002200D01* +X005241Y002170D01* +X005207Y002136D01* +X005176Y002099D01* +M02* diff --git a/testdata/proj1.drl b/testdata/proj1.drl new file mode 100644 index 0000000..750ab83 --- /dev/null +++ b/testdata/proj1.drl @@ -0,0 +1,17 @@ +T01 0.028in +T02 0.035in +T03 0.042in +T04 0.052in +T05 0.0595in +T06 0.086in +T07 0.125in +T08 0.152in +T09 0.025in +T10 0.032in +T11 0.1015in +T12 0.0670in +T13 0.0465in +T14 0.036in +T15 0.136in +T16 0.020in +T17 0.032in diff --git a/testdata/proj1.plc b/testdata/proj1.plc new file mode 100644 index 0000000..02aed27 --- /dev/null +++ b/testdata/proj1.plc @@ -0,0 +1,160 @@ +G75* +G70* +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0050*% +%ADD11C,0.0060*% +%ADD12C,0.0260*% +%ADD13R,0.2600X0.0240*% +D10* +X003450Y002300D02* +X003450Y002750D01* +X003300Y002750D02* +X003600Y002750D01* +X003760Y002750D02* +X003985Y002750D01* +X004060Y002675D01* +X004060Y002525D01* +X003985Y002450D01* +X003760Y002450D01* +X003760Y002300D02* +X003760Y002750D01* +X004221Y002525D02* +X004521Y002525D01* +X004446Y002300D02* +X004446Y002750D01* +X004221Y002525D01* +X002521Y002500D02* +X002221Y002500D01* +X002521Y002800D01* +X002521Y002875D01* +X002446Y002950D01* +X002296Y002950D01* +X002221Y002875D01* +X002060Y002875D02* +X002060Y002725D01* +X001985Y002650D01* +X001760Y002650D01* +X001760Y002500D02* +X001760Y002950D01* +X001985Y002950D01* +X002060Y002875D01* +X001600Y002950D02* +X001300Y002950D01* +X001450Y002950D02* +X001450Y002500D01* +X001400Y006150D02* +X001400Y006600D01* +X001250Y006600D02* +X001550Y006600D01* +X001710Y006600D02* +X001935Y006600D01* +X002010Y006525D01* +X002010Y006375D01* +X001935Y006300D01* +X001710Y006300D01* +X001710Y006150D02* +X001710Y006600D01* +X002171Y006525D02* +X002246Y006600D01* +X002396Y006600D01* +X002471Y006525D01* +X002471Y006450D01* +X002396Y006375D01* +X002471Y006300D01* +X002471Y006225D01* +X002396Y006150D01* +X002246Y006150D01* +X002171Y006225D01* +X002321Y006375D02* +X002396Y006375D01* +X007850Y006900D02* +X008150Y006900D01* +X008000Y006900D02* +X008000Y006450D01* +X008310Y006450D02* +X008310Y006900D01* +X008535Y006900D01* +X008610Y006825D01* +X008610Y006675D01* +X008535Y006600D01* +X008310Y006600D01* +X008771Y006450D02* +X009071Y006450D01* +X008921Y006450D02* +X008921Y006900D01* +X008771Y006750D01* +X009039Y004468D02* +X009039Y004167D01* +X008889Y004007D02* +X008589Y004007D01* +X008739Y004167D02* +X008589Y004317D01* +X009039Y004317D01* +X008889Y004007D02* +X009039Y003857D01* +X008889Y003707D01* +X008589Y003707D01* +X008664Y003547D02* +X008589Y003472D01* +X008589Y003322D01* +X008664Y003247D01* +X008739Y003247D01* +X008814Y003322D01* +X008814Y003472D01* +X008889Y003547D01* +X008964Y003547D01* +X009039Y003472D01* +X009039Y003322D01* +X008964Y003247D01* +X010289Y003848D02* +X010589Y004148D01* +X010589Y004223D01* +X010514Y004298D01* +X010364Y004298D01* +X010289Y004223D01* +X010289Y003848D02* +X010589Y003848D01* +X010489Y001478D02* +X010489Y001028D01* +X010339Y001028D02* +X010639Y001028D01* +X010339Y001328D02* +X010489Y001478D01* +D11* +X010664Y001773D02* +X010314Y001773D01* +X010314Y003643D01* +X010664Y003643D01* +X014314Y003643D01* +X014564Y003643D01* +X014914Y003843D01* +X014914Y004643D01* +X009714Y004643D01* +X009714Y003603D01* +X010664Y003643D02* +X010664Y001773D01* +X014314Y001773D01* +X014314Y003643D01* +X014314Y001773D02* +X014564Y001773D01* +X014914Y001573D01* +X014914Y000783D01* +X009714Y000783D01* +X009714Y001843D01* +X009714Y002623D02* +X009714Y002823D01* +D12* +X013214Y003217D02* +X013414Y003217D01* +X013414Y002233D02* +X013214Y002233D01* +D13* +X011964Y002233D03* +X011964Y003213D03* +M02* diff --git a/testdata/proj1.pls b/testdata/proj1.pls new file mode 100644 index 0000000..59b1528 --- /dev/null +++ b/testdata/proj1.pls @@ -0,0 +1,43 @@ +G75* +G70* +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0040*% +D10* +X003138Y007845D02* +X003445Y007845D01* +X003598Y007845D02* +X003751Y007998D01* +X003675Y007998D02* +X003905Y007998D01* +X003675Y007998D02* +X003598Y008075D01* +X003598Y008229D01* +X003675Y008305D01* +X003905Y008305D01* +X003905Y007845D01* +X003445Y008152D02* +X003291Y008305D01* +X003291Y007845D01* +X005588Y008195D02* +X005895Y008195D01* +X006048Y008195D02* +X006201Y008348D01* +X006048Y008272D02* +X006125Y008195D01* +X006278Y008195D01* +X006355Y008272D01* +X006355Y008579D01* +X006278Y008655D01* +X006125Y008655D01* +X006048Y008579D01* +X006048Y008272D01* +X005741Y008195D02* +X005741Y008655D01* +X005895Y008502D01* +M02* diff --git a/testdata/proj1.sch b/testdata/proj1.sch new file mode 100644 index 0000000..4a312fc Binary files /dev/null and b/testdata/proj1.sch differ diff --git a/testdata/proj1.sol b/testdata/proj1.sol new file mode 100644 index 0000000..aa1c9a7 --- /dev/null +++ b/testdata/proj1.sol @@ -0,0 +1,652 @@ +G75* +G70* +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10OC8,0.0850*% +%ADD11O,0.0780X0.1560*% +%ADD12O,0.1560X0.0780*% +%ADD13R,0.0394X0.0551*% +%ADD14R,0.0630X0.0787*% +%ADD15O,0.1200X0.0600*% +%ADD16C,0.0400*% +%ADD17C,0.0100*% +%ADD18R,0.0400X0.0400*% +D10* +X001625Y005625D03* +X008225Y005925D03* +D11* +X001625Y001625D03* +D12* +X003625Y001625D03* +D13* +X005251Y006992D03* +X005999Y006992D03* +X005625Y007858D03* +D14* +X003976Y007325D03* +X002874Y007325D03* +D15* +X009564Y003217D03* +X009564Y002233D03* +D16* +X010125Y008625D03* +D17* +X002165Y001011D02* +X002080Y000927D01* +X010760Y000927D01* +X010760Y009423D01* +X001190Y009423D01* +X001190Y006003D01* +X001387Y006200D01* +X001863Y006200D01* +X002200Y005863D01* +X002200Y005387D01* +X001863Y005050D01* +X001387Y005050D01* +X001190Y005247D01* +X001190Y002343D01* +X001401Y002555D01* +X001849Y002555D01* +X002165Y002239D01* +X002165Y001011D01* +X002165Y001019D02* +X010760Y001019D01* +X010760Y001118D02* +X004271Y001118D01* +X004239Y001085D02* +X004555Y001401D01* +X004555Y001849D01* +X004239Y002165D01* +X003011Y002165D01* +X002695Y001849D01* +X002695Y001401D01* +X003011Y001085D01* +X004239Y001085D01* +X004370Y001216D02* +X010760Y001216D01* +X010760Y001315D02* +X004468Y001315D01* +X004555Y001413D02* +X010760Y001413D01* +X010760Y001512D02* +X004555Y001512D01* +X004555Y001610D02* +X010760Y001610D01* +X010760Y001709D02* +X004555Y001709D01* +X004555Y001807D02* +X009053Y001807D01* +X009078Y001783D02* +X010051Y001783D01* +X010314Y002046D01* +X010314Y002419D01* +X010051Y002683D01* +X009078Y002683D01* +X008814Y002419D01* +X008814Y002046D01* +X009078Y001783D01* +X008955Y001906D02* +X004497Y001906D01* +X004399Y002004D02* +X008856Y002004D01* +X008814Y002103D02* +X004300Y002103D01* +X002949Y002103D02* +X002165Y002103D01* +X002165Y002004D02* +X002851Y002004D01* +X002752Y001906D02* +X002165Y001906D01* +X002165Y001807D02* +X002695Y001807D01* +X002695Y001709D02* +X002165Y001709D01* +X002165Y001610D02* +X002695Y001610D01* +X002695Y001512D02* +X002165Y001512D01* +X002165Y001413D02* +X002695Y001413D01* +X002781Y001315D02* +X002165Y001315D01* +X002165Y001216D02* +X002880Y001216D01* +X002979Y001118D02* +X002165Y001118D01* +X002165Y002202D02* +X008814Y002202D01* +X008814Y002300D02* +X002103Y002300D01* +X002005Y002399D02* +X008814Y002399D01* +X008892Y002497D02* +X001906Y002497D01* +X001344Y002497D02* +X001190Y002497D01* +X001190Y002399D02* +X001245Y002399D01* +X001190Y002596D02* +X008991Y002596D01* +X009078Y002767D02* +X010051Y002767D01* +X010314Y003031D01* +X010314Y003403D01* +X010051Y003667D01* +X009078Y003667D01* +X008814Y003403D01* +X008814Y003031D01* +X009078Y002767D01* +X009052Y002793D02* +X001190Y002793D01* +X001190Y002891D02* +X008953Y002891D01* +X008855Y002990D02* +X001190Y002990D01* +X001190Y003088D02* +X008814Y003088D01* +X008814Y003187D02* +X001190Y003187D01* +X001190Y003286D02* +X008814Y003286D01* +X008814Y003384D02* +X001190Y003384D01* +X001190Y003483D02* +X008894Y003483D01* +X008992Y003581D02* +X001190Y003581D01* +X001190Y003680D02* +X010760Y003680D01* +X010760Y003778D02* +X001190Y003778D01* +X001190Y003877D02* +X010760Y003877D01* +X010760Y003975D02* +X001190Y003975D01* +X001190Y004074D02* +X010760Y004074D01* +X010760Y004172D02* +X001190Y004172D01* +X001190Y004271D02* +X010760Y004271D01* +X010760Y004370D02* +X001190Y004370D01* +X001190Y004468D02* +X010760Y004468D01* +X010760Y004567D02* +X001190Y004567D01* +X001190Y004665D02* +X010760Y004665D01* +X010760Y004764D02* +X001190Y004764D01* +X001190Y004862D02* +X010760Y004862D01* +X010760Y004961D02* +X001190Y004961D01* +X001190Y005059D02* +X001377Y005059D01* +X001279Y005158D02* +X001190Y005158D01* +X001872Y005059D02* +X005501Y005059D01* +X005511Y005055D02* +X005738Y005055D01* +X005948Y005142D01* +X006108Y005302D01* +X006195Y005511D01* +X006195Y005738D01* +X006108Y005948D01* +X005948Y006108D01* +X005738Y006195D01* +X005511Y006195D01* +X005302Y006108D01* +X005142Y005948D01* +X005055Y005738D01* +X005055Y005511D01* +X005142Y005302D01* +X005302Y005142D01* +X005511Y005055D01* +X005286Y005158D02* +X001971Y005158D01* +X002070Y005256D02* +X005187Y005256D01* +X005120Y005355D02* +X002168Y005355D01* +X002200Y005454D02* +X005079Y005454D01* +X005055Y005552D02* +X002200Y005552D01* +X002200Y005651D02* +X005055Y005651D01* +X005059Y005749D02* +X002200Y005749D01* +X002200Y005848D02* +X005100Y005848D01* +X005141Y005946D02* +X002117Y005946D01* +X002018Y006045D02* +X005239Y006045D01* +X005387Y006143D02* +X001920Y006143D01* +X001330Y006143D02* +X001190Y006143D01* +X001190Y006045D02* +X001232Y006045D01* +X001190Y006242D02* +X003325Y006242D01* +X003342Y006225D02* +X005542Y006225D01* +X005708Y006225D01* +X006049Y006566D01* +X006258Y006566D01* +X006346Y006654D01* +X006346Y007330D01* +X006258Y007417D01* +X005740Y007417D01* +X005652Y007330D01* +X005652Y006735D01* +X005598Y006680D01* +X005598Y007330D01* +X005510Y007417D01* +X004992Y007417D01* +X004904Y007330D01* +X004904Y006654D01* +X004933Y006625D01* +X003508Y006625D01* +X003301Y006831D01* +X003339Y006869D01* +X003339Y007781D01* +X003251Y007869D01* +X002497Y007869D01* +X002409Y007781D01* +X002409Y006869D01* +X002497Y006781D01* +X002786Y006781D01* +X003225Y006342D01* +X003342Y006225D01* +X003227Y006340D02* +X001190Y006340D01* +X001190Y006439D02* +X003128Y006439D01* +X003029Y006537D02* +X001190Y006537D01* +X001190Y006636D02* +X002931Y006636D01* +X002832Y006735D02* +X001190Y006735D01* +X001190Y006833D02* +X002445Y006833D01* +X002409Y006932D02* +X001190Y006932D01* +X001190Y007030D02* +X002409Y007030D01* +X002409Y007129D02* +X001190Y007129D01* +X001190Y007227D02* +X002409Y007227D01* +X002409Y007326D02* +X001190Y007326D01* +X001190Y007424D02* +X002409Y007424D01* +X002409Y007523D02* +X001190Y007523D01* +X001190Y007621D02* +X002409Y007621D01* +X002409Y007720D02* +X001190Y007720D01* +X001190Y007819D02* +X002447Y007819D01* +X002874Y007325D02* +X002874Y006976D01* +X003425Y006425D01* +X005625Y006425D01* +X006025Y006825D01* +X005999Y006851D01* +X005999Y006992D01* +X005652Y007030D02* +X005598Y007030D01* +X005598Y006932D02* +X005652Y006932D01* +X005652Y006833D02* +X005598Y006833D01* +X005598Y006735D02* +X005652Y006735D01* +X006020Y006537D02* +X010760Y006537D01* +X010760Y006439D02* +X008524Y006439D01* +X008463Y006500D02* +X008275Y006500D01* +X008275Y005975D01* +X008800Y005975D01* +X008800Y006163D01* +X008463Y006500D01* +X008275Y006439D02* +X008175Y006439D01* +X008175Y006500D02* +X007987Y006500D01* +X007650Y006163D01* +X007650Y005975D01* +X008175Y005975D01* +X008175Y006500D01* +X008175Y006340D02* +X008275Y006340D01* +X008275Y006242D02* +X008175Y006242D01* +X008175Y006143D02* +X008275Y006143D01* +X008275Y006045D02* +X008175Y006045D01* +X008175Y005975D02* +X008275Y005975D01* +X008275Y005875D01* +X008800Y005875D01* +X008800Y005687D01* +X008463Y005350D01* +X008275Y005350D01* +X008275Y005875D01* +X008175Y005875D01* +X008175Y005350D01* +X007987Y005350D01* +X007650Y005687D01* +X007650Y005875D01* +X008175Y005875D01* +X008175Y005975D01* +X008175Y005946D02* +X006109Y005946D01* +X006150Y005848D02* +X007650Y005848D01* +X007650Y005749D02* +X006190Y005749D01* +X006195Y005651D02* +X007686Y005651D01* +X007785Y005552D02* +X006195Y005552D01* +X006171Y005454D02* +X007883Y005454D01* +X007982Y005355D02* +X006130Y005355D01* +X006063Y005256D02* +X010760Y005256D01* +X010760Y005158D02* +X005964Y005158D01* +X005749Y005059D02* +X010760Y005059D01* +X010760Y005355D02* +X008468Y005355D01* +X008567Y005454D02* +X010760Y005454D01* +X010760Y005552D02* +X008665Y005552D01* +X008764Y005651D02* +X010760Y005651D01* +X010760Y005749D02* +X008800Y005749D01* +X008800Y005848D02* +X010760Y005848D01* +X010760Y005946D02* +X008275Y005946D01* +X008275Y005848D02* +X008175Y005848D01* +X008175Y005749D02* +X008275Y005749D01* +X008275Y005651D02* +X008175Y005651D01* +X008175Y005552D02* +X008275Y005552D01* +X008275Y005454D02* +X008175Y005454D01* +X008175Y005355D02* +X008275Y005355D01* +X008800Y006045D02* +X010760Y006045D01* +X010760Y006143D02* +X008800Y006143D01* +X008721Y006242D02* +X010760Y006242D01* +X010760Y006340D02* +X008622Y006340D01* +X007926Y006439D02* +X005922Y006439D01* +X005823Y006340D02* +X007827Y006340D01* +X007729Y006242D02* +X005725Y006242D01* +X005863Y006143D02* +X007650Y006143D01* +X007650Y006045D02* +X006011Y006045D01* +X006328Y006636D02* +X010760Y006636D01* +X010760Y006735D02* +X006346Y006735D01* +X006346Y006833D02* +X010760Y006833D01* +X010760Y006932D02* +X006346Y006932D01* +X006346Y007030D02* +X010760Y007030D01* +X010760Y007129D02* +X006346Y007129D01* +X006346Y007227D02* +X010760Y007227D01* +X010760Y007326D02* +X006346Y007326D01* +X005972Y007520D02* +X005884Y007432D01* +X005366Y007432D01* +X005278Y007520D01* +X005278Y007658D01* +X004441Y007658D01* +X004441Y006869D01* +X004353Y006781D01* +X003599Y006781D01* +X003511Y006869D01* +X003511Y007781D01* +X003599Y007869D01* +X003825Y007869D01* +X003825Y007941D01* +X003942Y008058D01* +X004108Y008058D01* +X005278Y008058D01* +X005278Y008196D01* +X005366Y008284D01* +X005884Y008284D01* +X005972Y008196D01* +X005972Y007520D01* +X005972Y007523D02* +X010760Y007523D01* +X010760Y007621D02* +X005972Y007621D01* +X005972Y007720D02* +X009818Y007720D01* +X009775Y007763D02* +X009863Y007675D01* +X010387Y007675D01* +X010475Y007763D01* +X010475Y008287D01* +X010387Y008375D01* +X010370Y008375D01* +X010422Y008427D01* +X010475Y008555D01* +X010475Y008694D01* +X010422Y008823D01* +X010323Y008922D01* +X010194Y008975D01* +X010055Y008975D01* +X009927Y008922D01* +X009828Y008823D01* +X009775Y008694D01* +X009775Y008555D01* +X009828Y008427D01* +X009880Y008375D01* +X009863Y008375D01* +X009775Y008287D01* +X009775Y007763D01* +X009775Y007819D02* +X005972Y007819D01* +X005972Y007917D02* +X009775Y007917D01* +X009775Y008016D02* +X005972Y008016D01* +X005972Y008114D02* +X009775Y008114D01* +X009775Y008213D02* +X005955Y008213D01* +X005625Y007858D02* +X004025Y007858D01* +X004025Y007325D01* +X003976Y007325D01* +X003511Y007326D02* +X003339Y007326D01* +X003339Y007424D02* +X003511Y007424D01* +X003511Y007523D02* +X003339Y007523D01* +X003339Y007621D02* +X003511Y007621D01* +X003511Y007720D02* +X003339Y007720D01* +X003301Y007819D02* +X003549Y007819D01* +X003825Y007917D02* +X001190Y007917D01* +X001190Y008016D02* +X003900Y008016D01* +X004441Y007621D02* +X005278Y007621D01* +X005278Y007523D02* +X004441Y007523D01* +X004441Y007424D02* +X010760Y007424D01* +X010760Y007720D02* +X010432Y007720D01* +X010475Y007819D02* +X010760Y007819D01* +X010760Y007917D02* +X010475Y007917D01* +X010475Y008016D02* +X010760Y008016D01* +X010760Y008114D02* +X010475Y008114D01* +X010475Y008213D02* +X010760Y008213D01* +X010760Y008311D02* +X010451Y008311D01* +X010405Y008410D02* +X010760Y008410D01* +X010760Y008508D02* +X010455Y008508D01* +X010475Y008607D02* +X010760Y008607D01* +X010760Y008705D02* +X010470Y008705D01* +X010430Y008804D02* +X010760Y008804D01* +X010760Y008903D02* +X010342Y008903D01* +X010760Y009001D02* +X001190Y009001D01* +X001190Y008903D02* +X009908Y008903D01* +X009820Y008804D02* +X001190Y008804D01* +X001190Y008705D02* +X009779Y008705D01* +X009775Y008607D02* +X001190Y008607D01* +X001190Y008508D02* +X009794Y008508D01* +X009845Y008410D02* +X001190Y008410D01* +X001190Y008311D02* +X009799Y008311D01* +X010760Y009100D02* +X001190Y009100D01* +X001190Y009198D02* +X010760Y009198D01* +X010760Y009297D02* +X001190Y009297D01* +X001190Y009395D02* +X010760Y009395D01* +X005652Y007326D02* +X005598Y007326D01* +X005598Y007227D02* +X005652Y007227D01* +X005652Y007129D02* +X005598Y007129D01* +X004904Y007129D02* +X004441Y007129D01* +X004441Y007227D02* +X004904Y007227D01* +X004904Y007326D02* +X004441Y007326D01* +X004441Y007030D02* +X004904Y007030D01* +X004904Y006932D02* +X004441Y006932D01* +X004405Y006833D02* +X004904Y006833D01* +X004904Y006735D02* +X003398Y006735D01* +X003303Y006833D02* +X003547Y006833D01* +X003511Y006932D02* +X003339Y006932D01* +X003339Y007030D02* +X003511Y007030D01* +X003511Y007129D02* +X003339Y007129D01* +X003339Y007227D02* +X003511Y007227D01* +X003496Y006636D02* +X004922Y006636D01* +X005278Y008114D02* +X001190Y008114D01* +X001190Y008213D02* +X005295Y008213D01* +X010136Y003581D02* +X010760Y003581D01* +X010760Y003483D02* +X010235Y003483D01* +X010314Y003384D02* +X010760Y003384D01* +X010760Y003286D02* +X010314Y003286D01* +X010314Y003187D02* +X010760Y003187D01* +X010760Y003088D02* +X010314Y003088D01* +X010274Y002990D02* +X010760Y002990D01* +X010760Y002891D02* +X010175Y002891D01* +X010076Y002793D02* +X010760Y002793D01* +X010760Y002694D02* +X001190Y002694D01* +X010075Y001807D02* +X010760Y001807D01* +X010760Y001906D02* +X010174Y001906D01* +X010272Y002004D02* +X010760Y002004D01* +X010760Y002103D02* +X010314Y002103D01* +X010314Y002202D02* +X010760Y002202D01* +X010760Y002300D02* +X010314Y002300D01* +X010314Y002399D02* +X010760Y002399D01* +X010760Y002497D02* +X010236Y002497D01* +X010138Y002596D02* +X010760Y002596D01* +D18* +X010125Y008025D03* +M02* diff --git a/testdata/proj1.stc b/testdata/proj1.stc new file mode 100644 index 0000000..75aa9f3 --- /dev/null +++ b/testdata/proj1.stc @@ -0,0 +1,33 @@ +G75* +G70* +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0320*% +%ADD11OC8,0.0930*% +%ADD12O,0.0860X0.1640*% +%ADD13O,0.1640X0.0860*% +%ADD14O,0.1280X0.0680*% +%ADD15C,0.0480*% +%ADD16R,0.0480X0.0480*% +D10* +X005625Y005625D03* +D11* +X008225Y005925D03* +X001625Y005625D03* +D12* +X001625Y001625D03* +D13* +X003625Y001625D03* +D14* +X009564Y002233D03* +X009564Y003217D03* +D15* +X010125Y008625D03* +D16* +X010125Y008025D03* +M02* diff --git a/testdata/proj1.sts b/testdata/proj1.sts new file mode 100644 index 0000000..bf96990 --- /dev/null +++ b/testdata/proj1.sts @@ -0,0 +1,42 @@ +G75* +G70* +%OFA0B0*% +%FSLAX24Y24*% +%IPPOS*% +%LPD*% +%AMOC8* +5,1,8,0,0,1.08239X$1,22.5* +% +%ADD10C,0.0320*% +%ADD11OC8,0.0930*% +%ADD12O,0.0860X0.1640*% +%ADD13O,0.1640X0.0860*% +%ADD14R,0.0474X0.0631*% +%ADD15R,0.0710X0.0867*% +%ADD16O,0.1280X0.0680*% +%ADD17C,0.0480*% +%ADD18R,0.0480X0.0480*% +D10* +X005625Y005625D03* +D11* +X008225Y005925D03* +X001625Y005625D03* +D12* +X001625Y001625D03* +D13* +X003625Y001625D03* +D14* +X005251Y006992D03* +X005999Y006992D03* +X005625Y007858D03* +D15* +X003976Y007325D03* +X002874Y007325D03* +D16* +X009564Y003217D03* +X009564Y002233D03* +D17* +X010125Y008625D03* +D18* +X010125Y008025D03* +M02* diff --git a/testdata/proj1.xln b/testdata/proj1.xln new file mode 100644 index 0000000..743e298 --- /dev/null +++ b/testdata/proj1.xln @@ -0,0 +1,20 @@ +% +M48 +M72 +T01C0.0240 +T02C0.0400 +T03C0.0520 +% +T01 +X5625Y5625 +X10125Y8025 +X10125Y8625 +T02 +X9564Y3217 +X9564Y2233 +T03 +X1625Y1625 +X3625Y1625 +X1625Y5625 +X8225Y5925 +M30