├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── basics └── basics.py ├── dxf_stuff ├── __init__.py ├── bulge.py ├── dxf_plugins.py ├── dxf_to_graphic.py ├── dxf_utils.py ├── mounting.py ├── orient_to_polys.py ├── pcbpoint.py └── recipe.py ├── gen_border ├── __init__.py └── gen_border.py ├── gensvg └── gensvg.py ├── instantiate_footprint ├── __init__.py └── instantiate_footprint.py ├── menus_and_buttons ├── hello.png └── menus_and_buttons.py ├── place_by_sch ├── __init__.py └── place_by_sch.py ├── plantuml ├── README ├── pcbnew_uml.png └── pcbnew_uml.puml ├── python_usage_tracker └── kicad_python_usage_tracker.py ├── ratnest ├── __init__.py └── ratnest.py ├── replicatelayout └── replicatelayout.py ├── save_config ├── __init__.py └── save_config.py ├── simpledialog ├── DialogUtils.py ├── __init__.py └── simpledialog.py ├── simplegui └── simplegui.py ├── svg2border ├── __init__.py ├── drawing.svg ├── parse_svg_path.py ├── svg2border.py ├── test_hole.py └── test_parser.py ├── toggle_visibility ├── __init__.py └── toggle_visibility.py ├── utils ├── __init__.py ├── delaunay.py ├── groundvias.py ├── util_plugins.py └── via_fill.py └── zonebug └── zonebug.py /.gitignore: -------------------------------------------------------------------------------- 1 | # emacs stuff 2 | *~ 3 | */*~ 4 | #*# 5 | */#*# 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | env/ 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *,cover 52 | .hypothesis/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # IPython Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # dotenv 85 | .env 86 | 87 | # virtualenv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kicad_mmccoo 2 | 3 | Kicad's pcbnew has a built in python engine for automating tasks. Unfortunately, this scripting interface is not documented anywhere that I've been able to find. 4 | 5 | This is a repository to hold example and utility code in the hopes that others can learn to write their own scripts. 6 | 7 | I have a [companion blog](https://kicad.mmccoo.com/) 8 | 9 | 10 | If you get nothing else out of this repository, take a look at this UML diagram. It doesn't contain all available APIs; just the ones needed to do common tasks (the spec from which it was generated is in the plantuml subdirectory) 11 | 12 | 13 | ![UML](plantuml/pcbnew_uml.png) 14 | 15 | # plugins 16 | This repo also contains a bunch of plugins, a description of which can be found in this youtube vide: 17 | 18 | [![YouTube video about the plugins](https://img.youtube.com/vi/VqegD7XZoFI/0.jpg)](https://www.youtube.com/watch?v=VqegD7XZoFI) 19 | 20 | ## plugin install 21 | For linux... create a directory in your homedir called .kicad_plugins 22 | Inside of that directory either clone this repo or link to a clone of it. 23 | 24 | ## additional python packages are needed 25 | # To get the dxf related plugins to work on linux systems, you'll need something like this: 26 | # Make sure pip is available 27 | sudo python2.7 -m ensurepip --default-pip 28 | # or 29 | sudo apt install python-pip 30 | 31 | # then these 32 | sudo pip2 install --upgrade pip 33 | sudo pip2 install dxfgrabber 34 | sudo pip2 install numpy 35 | sudo pip2 install scipy 36 | sudo pip2 install shapely -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # this file is here to make the external plugins of this repo available from the pcbnew menu. 2 | # to make these plugins available in your kicad, you'll need to have then be available here: 3 | # ~/ubuntu/.kicad_plugins/ 4 | #in other worked ~/ubuntu/.kicad_plugins/kicad_mmccooo 5 | 6 | # for these particular plugins, you'll need dxfgrabber, numpy, scipy, shapely. 7 | # note that kicad is still on python 2.7. 8 | # sudo python2.7 -m ensurepip --default-pip 9 | # or 10 | # sudo apt install python-pip 11 | 12 | 13 | # sudo pip2 install --upgrade pip 14 | # sudo pip2 install dxfgrabber 15 | # sudo pip2 install numpy 16 | # sudo pip2 install scipy 17 | # sudo pip2 install shapely 18 | 19 | import pcbnew 20 | 21 | print("initializing mmccoo_kicad") 22 | 23 | import gen_border 24 | import dxf_stuff 25 | import place_by_sch 26 | import instantiate_footprint 27 | import toggle_visibility 28 | 29 | # I don't think it's possible to control ratsnets for individual nets. 30 | # It used to be possible, but not since the new connectivity algorithm. 31 | # import ratnest 32 | 33 | import utils 34 | import svg2border 35 | print("done adding mmccoo_kicad") 36 | -------------------------------------------------------------------------------- /basics/basics.py: -------------------------------------------------------------------------------- 1 | # Copyright [2017] [Miles McCoo] 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | # this file is some basic examples of doing scripting in pcbnew's python 17 | # interface. 18 | 19 | 20 | 21 | import pcbnew 22 | 23 | # most queries start with a board 24 | board = pcbnew.GetBoard() 25 | 26 | ###### 27 | # NETS 28 | ###### 29 | 30 | # want to know all of the nets in your board? 31 | # nets can be looked up in two ways: 32 | # by name 33 | # by netcode - a unique integer identifier for your net. 34 | 35 | # returns a dictionary netcode:netinfo_item 36 | netcodes = board.GetNetsByNetcode() 37 | 38 | # list off all of the nets in the board. 39 | for netcode, net in netcodes.items(): 40 | print("netcode {}, name {}".format(netcode, net.GetNetname())) 41 | 42 | 43 | # here's another way of doing the same thing. 44 | print("here's the other way to do it") 45 | nets = board.GetNetsByName() 46 | for netname, net in nets.items(): 47 | print("method2 netcode {}, name{}".format(net.GetNet(), netname)) 48 | 49 | 50 | # maybe you just want a single net 51 | # the find method returns an iterator to all matching nets. 52 | # the value of an iterator is a tuple: name, netinfo 53 | neti = nets.find("/clk") 54 | if (neti != nets.end()): 55 | clknet = neti.value()[1] 56 | clkclass = clknet.GetNetClass() 57 | 58 | print("net {} is on netclass {}".format(clknet.GetNetname(), 59 | clkclass)) 60 | 61 | ######## 62 | # tracks 63 | ######## 64 | 65 | # clk net was defined above as was SCALE 66 | clktracks = board.TracksInNet(clknet.GetNet()) 67 | for track in clktracks: 68 | print("{},{}->{},{} width {} layer {}".format(track.GetStart().x/SCALE, 69 | track.GetStart().y/SCALE, 70 | track.GetEnd().x/SCALE, 71 | track.GetEnd().y/SCALE, 72 | track.GetWidth()/SCALE, 73 | layertable[track.GetLayer()])) 74 | else: 75 | print("you don't have a net call clk. change the find('clk') to look for a net you do have") 76 | 77 | 78 | ##################### 79 | # physical dimensions 80 | ##################### 81 | 82 | # coordinate space of kicad_pcb is in mm. At the beginning of 83 | # https://en.wikibooks.org/wiki/Kicad/file_formats#Board_File_Format 84 | # "All physical units are in mils (1/1000th inch) unless otherwise noted." 85 | # then later in historical notes, it says, 86 | # As of 2013, the PCBnew application creates ".kicad_pcb" files that begin with 87 | # "(kicad_pcb (version 3)". All distances are in millimeters. 88 | 89 | # the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm) 90 | # the coordinate 121550000 corresponds to 121.550000 91 | 92 | SCALE = 1000000.0 93 | 94 | boardbbox = board.ComputeBoundingBox() 95 | boardxl = boardbbox.GetX() 96 | boardyl = boardbbox.GetY() 97 | boardwidth = boardbbox.GetWidth() 98 | boardheight = boardbbox.GetHeight() 99 | 100 | print("this board is at position {},{} {} wide and {} high".format(boardxl, 101 | boardyl, 102 | boardwidth, 103 | boardheight)) 104 | 105 | # each of your placed modules can be found with its reference name 106 | # the module connection points are pad, of course. 107 | 108 | 109 | padshapes = { 110 | pcbnew.PAD_SHAPE_CIRCLE: "PAD_SHAPE_CIRCLE", 111 | pcbnew.PAD_SHAPE_OVAL: "PAD_SHAPE_OVAL", 112 | pcbnew.PAD_SHAPE_RECT: "PAD_SHAPE_RECT", 113 | pcbnew.PAD_SHAPE_TRAPEZOID: "PAD_SHAPE_TRAPEZOID" 114 | } 115 | # new in the most recent kicad code 116 | if hasattr(pcbnew, 'PAD_SHAPE_ROUNDRECT'): 117 | padshapes[pcbnew.PAD_SHAPE_ROUNDRECT] = "PAD_SHAPE_ROUNDRECT", 118 | 119 | 120 | modref = "U1" 121 | mod = board.FindModuleByReference(modref) 122 | if (mod == None): 123 | print("you don't have a module named U1 which this script assumes is there.") 124 | print("search for U1 in the script and change it to something you do have") 125 | 126 | for pad in mod.Pads(): 127 | print("pad {}({}) on {}({}) at {},{} shape {} size {},{}" 128 | .format(pad.GetPadName(), 129 | pad.GetNet().GetNetname(), 130 | mod.GetReference(), 131 | mod.GetValue(), 132 | pad.GetPosition().x, pad.GetPosition().y, 133 | padshapes[pad.GetShape()], 134 | pad.GetSize().x, pad.GetSize().y 135 | )) 136 | 137 | ######## 138 | # layers 139 | ######## 140 | 141 | layertable = {} 142 | 143 | numlayers = pcbnew.LAYER_ID_COUNT 144 | for i in range(numlayers): 145 | layertable[i] = board.GetLayerName(i) 146 | print("{} {}".format(i, board.GetLayerName(i))) 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /dxf_stuff/__init__.py: -------------------------------------------------------------------------------- 1 | import pdb 2 | 3 | try: 4 | import dxfgrabber 5 | import numpy 6 | import scipy 7 | import shapely 8 | except ImportError as error: 9 | 10 | print("unable to import needed libraries. dxf stuff won't work.") 11 | print(error.message) 12 | print(""" 13 | # To get it to work on linux systems, you'll need something like this: 14 | # Make sure pip is available 15 | sudo python2.7 -m ensurepip --default-pip 16 | # or 17 | sudo apt install python-pip 18 | 19 | # then these 20 | sudo pip2 install --upgrade pip 21 | sudo pip2 install dxfgrabber 22 | sudo pip2 install numpy 23 | sudo pip2 install scipy 24 | sudo pip2 install shapely 25 | """) 26 | else: 27 | import dxf_plugins 28 | -------------------------------------------------------------------------------- /dxf_stuff/bulge.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import math 3 | 4 | def polar(pt, ang, dist): 5 | return (pt[0] + dist*np.cos(ang), 6 | pt[1] + dist*np.sin(ang)) 7 | 8 | # 2pi is 360 degrees 9 | # pi is 180 10 | # pi/2 is 90 11 | # pi/4 is 45 12 | #print(polar((10,10), np.pi/2, 20)) 13 | 14 | def angle(pt1, pt2): 15 | return math.atan2(pt2[1]-pt1[1], pt2[0]-pt1[0]) 16 | 17 | #print(angle((10,10), (10,20))/np.pi) 18 | 19 | # bulge is described here : 20 | # http://www.lee-mac.com/bulgeconversion.html 21 | # which used the function: polar described here: 22 | # http://www.afralisp.net/autolisp/tutorials/calculating-polar-points.php 23 | # 2*pi is 360 degrees 24 | 25 | 26 | # the function and text below is a translation of this: 27 | # http://www.lee-mac.com/bulgeconversion.html 28 | # ;; Bulge to Arc - Lee Mac 29 | # ;; p1 - start vertex 30 | # ;; p2 - end vertex 31 | # ;; b - bulge 32 | # ;; Returns: (
) 33 | 34 | # (defun LM:Bulge->Arc ( p1 p2 b / a c r ) 35 | # (setq a (* 2 (atan b)) 36 | # r (/ (distance p1 p2) 2 (sin a)) 37 | # c (polar p1 (+ (- (/ pi 2) a) (angle p1 p2)) r) 38 | # ) 39 | # (if (minusp b) 40 | # (list c (angle c p2) (angle c p1) (abs r)) 41 | # (list c (angle c p1) (angle c p2) (abs r)) 42 | # ) 43 | # ) 44 | def bulge2arc(p1, p2, bulge): 45 | a = 2*np.arctan(bulge) 46 | r = math.hypot(p2[0]-p1[0], p2[1]-p1[1]) / 2.0 / np.sin(a) 47 | c = polar(p1, np.pi/2.0-a + angle(p1, p2), r) 48 | 49 | # in the code I stole above, the start and end angles are reversed 50 | # if bulge is negative. Even from reading the comments on that page, 51 | # I don't see why that is done. 52 | # later edit. The page also has this line: 53 | # "The returned bulge value may be positive or negative, depending 54 | # upon whether the arc passing through the three points traces a clockwise 55 | # or counter-clockwise path." 56 | # negative indicates clockwise. 57 | # so if going from point A->B at 90 degrees to -90 degress with bulge -1, we pass through 0 58 | # if going from -90 to 90 also with bulge -1, we don't pass through 0. 59 | #if (bulge<0): 60 | # return (c, np.rad2deg(angle(c, p2)), np.rad2deg(angle(c, p1)), np.absolute(r)) 61 | #else: 62 | a1 = np.rad2deg(angle(c, p1)) 63 | a2 = np.rad2deg(angle(c, p2)) 64 | # if clockwise (bulge<0) and the angles go from negative to positive, we 65 | # don't cross 0 degress. 66 | if (a1<0) and (a2>0) and (bulge<0): 67 | a1 = a1+360.0 68 | # if counterclockwise (bulge>0) and the angles go from positive to negative 69 | # we also don't cross 0 degrees 70 | if (a1>0) and (a2<0) and (bulge>0): 71 | a2 = a2+360.0 72 | return (c, a1, a2, np.absolute(r)) 73 | 74 | #print(bulge2arc((0,10), (10,0), .5)) 75 | -------------------------------------------------------------------------------- /dxf_stuff/dxf_plugins.py: -------------------------------------------------------------------------------- 1 | 2 | import pcbnew 3 | import wx 4 | import os 5 | import sys 6 | import inspect 7 | import pdb 8 | 9 | import dxf_utils 10 | from ..simpledialog import DialogUtils 11 | 12 | import os.path 13 | 14 | from dxf_utils import zone_actions 15 | from dxf_utils import segment_actions 16 | from dxf_utils import orient_actions 17 | from dxf_utils import mounting_actions 18 | from dxf_utils import traverse_dxf 19 | from dxf_utils import traverse_graphics 20 | import mounting 21 | 22 | class DXFZoneDialog(DialogUtils.BaseDialog): 23 | def __init__(self): 24 | super(DXFZoneDialog, self).__init__("DXF Dialog") 25 | 26 | homedir = os.path.expanduser("~") 27 | self.file_picker = DialogUtils.FilePicker(self, homedir, 28 | wildcard="DXF files (.dxf)|*.dxf", 29 | configname="DXFZonedialog") 30 | self.AddLabeled(item=self.file_picker, label="DXF file", 31 | proportion=0, flag=wx.ALL, border=2) 32 | 33 | self.basic_layer = DialogUtils.BasicLayerPicker(self, layers=['F.Cu', 'B.Cu']) 34 | self.AddLabeled(item=self.basic_layer, label="Target layer", border=2) 35 | 36 | self.net = DialogUtils.NetPicker(self) 37 | self.AddLabeled(item=self.net, 38 | label="Target Net", 39 | proportion=1, 40 | flag=wx.EXPAND|wx.ALL, 41 | border=2) 42 | 43 | # make the dialog a little taller than minimum to give the layer and net 44 | # lists a bit more space. 45 | self.IncSize(height=5) 46 | 47 | class DXFZonePlugin(pcbnew.ActionPlugin): 48 | def defaults(self): 49 | self.name = "Convert a DXF to a zone" 50 | self.category = "A descriptive category name" 51 | self.description = "This plugin reads a dxf file and converts it to a zone" 52 | 53 | def Run(self): 54 | dlg = DXFZoneDialog() 55 | res = dlg.ShowModal() 56 | 57 | if res == wx.ID_OK: 58 | print("ok") 59 | if (dlg.net.value == None): 60 | warndlg = wx.MessageDialog(self, "no net was selected", "Error", wx.OK | wx.ICON_WARNING) 61 | warndlg.ShowModal() 62 | warndlg.Destroy() 63 | return 64 | 65 | net = dlg.net.GetValuePtr() 66 | 67 | traverse_dxf(dlg.file_picker.value, 68 | zone_actions(pcbnew.GetBoard(), 69 | net, 70 | dlg.basic_layer.valueint), 71 | merge_polys=True, 72 | break_curves=True 73 | ) 74 | #pcbnew.Refresh() 75 | else: 76 | print("cancel") 77 | 78 | DXFZonePlugin().register() 79 | 80 | class DXFGraphicDialog(DialogUtils.BaseDialog): 81 | def __init__(self): 82 | super(DXFGraphicDialog, self).__init__("DXF Dialog") 83 | 84 | homedir = os.path.expanduser("~") 85 | self.file_picker = DialogUtils.FilePicker(self, homedir, 86 | wildcard="DXF files (.dxf)|*.dxf", 87 | configname="DXFGraphicdialog") 88 | self.AddLabeled(item=self.file_picker, label="DXF file", 89 | proportion=0, flag=wx.ALL, border=2) 90 | 91 | self.basic_layer = DialogUtils.BasicLayerPicker(self) 92 | self.AddLabeled(item=self.basic_layer, label="Target layer", border=2) 93 | 94 | 95 | 96 | class DXFGraphicPlugin(pcbnew.ActionPlugin): 97 | def defaults(self): 98 | self.name = "Convert a DXF to a graphic (Edge.Cuts, User.Cmts,...)" 99 | self.category = "A descriptive category name" 100 | self.description = "This plugin reads a dxf file and converts it to a zone" 101 | 102 | def Run(self): 103 | dlg = DXFGraphicDialog() 104 | res = dlg.ShowModal() 105 | 106 | print("dxf file {}".format(dlg.file_picker.value)) 107 | print("layer {}".format(dlg.basic_layer.value)) 108 | 109 | if res == wx.ID_OK: 110 | print("ok") 111 | traverse_dxf(dlg.file_picker.value, 112 | segment_actions(pcbnew.GetBoard(), dlg.basic_layer.valueint), 113 | merge_polys=False, 114 | break_curves=True) 115 | 116 | else: 117 | print("cancel") 118 | 119 | DXFGraphicPlugin().register() 120 | 121 | class OrientToGraphicDialog(DialogUtils.BaseDialog): 122 | def __init__(self): 123 | super(OrientToGraphicDialog, self).__init__("Orient to Graphic") 124 | 125 | self.basic_layer = DialogUtils.BasicLayerPicker(self, layers=['Cmts.User', 'Eco1.User', 'Eco2.User']) 126 | self.AddLabeled(item=self.basic_layer, label="Target layer", border=2) 127 | 128 | self.mods = DialogUtils.ModulePicker(self, singleton=False) 129 | self.AddLabeled(item=self.mods, 130 | label="all mods", 131 | proportion=1, 132 | flag=wx.EXPAND|wx.ALL, 133 | border=2) 134 | 135 | class OrientToGraphicPlugin(pcbnew.ActionPlugin): 136 | def defaults(self): 137 | self.name = "Orient the selected modules to underlying graphics" 138 | self.category = "A descriptive category name" 139 | self.description = "This plugin moves/orients selected modules to align with graphic" 140 | 141 | def Run(self): 142 | dlg = OrientToGraphicDialog() 143 | res = dlg.ShowModal() 144 | 145 | print("layer {}".format(dlg.basic_layer.value)) 146 | print("mods {}".format(dlg.mods.value)) 147 | 148 | traverse_graphics(pcbnew.GetBoard(), dlg.basic_layer.value, 149 | orient_actions(pcbnew.GetBoard(), dlg.mods.value), 150 | merge_polys=True, 151 | break_curves=True) 152 | 153 | 154 | OrientToGraphicPlugin().register() 155 | 156 | class DXFToMountingPlugin(pcbnew.ActionPlugin): 157 | def defaults(self): 158 | self.name = "DXF circles to modules" 159 | self.category = "A descriptive category name" 160 | self.description = "This plugin places a module for each circle in a DXF" 161 | 162 | def Run(self): 163 | dlg = mounting.MountingDialog(configname = "mountingmap") 164 | res = dlg.ShowModal() 165 | 166 | if res != wx.ID_OK: 167 | return 168 | 169 | 170 | traverse_dxf(dlg.file_picker.value, 171 | mounting_actions(pcbnew.GetBoard(), 172 | dlg.value, 173 | flip=dlg.flip.GetValue())) 174 | 175 | 176 | 177 | DXFToMountingPlugin().register() 178 | -------------------------------------------------------------------------------- /dxf_stuff/dxf_to_graphic.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | # http://pythonhosted.org/dxfgrabber/#dxfgrabber 4 | 5 | import pcbnew 6 | import dxfgrabber 7 | import re 8 | import sys, os.path, inspect 9 | import numpy as np 10 | 11 | oldpath = sys.path 12 | # inspect.stack()[0][1] is the full path to the current file. 13 | sys.path.insert(0, os.path.dirname(inspect.stack()[0][1])) 14 | import bulge 15 | import pcbpoint 16 | sys.path = oldpath 17 | 18 | 19 | # the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm) 20 | # the coordinate 121550000 corresponds to 121.550000 21 | SCALE = 1000000.0 22 | 23 | 24 | 25 | type_table = {} 26 | for t in filter(lambda t: re.match("PCB_.*_T", t), dir(pcbnew)): 27 | type_table[getattr(pcbnew, t)] = t 28 | 29 | shape_table = {} 30 | for s in filter(lambda s: re.match("S_.*", s), dir(pcbnew)): 31 | shape_table[getattr(pcbnew, s)] = s 32 | 33 | # generate a name->layer table so we can lookup layer numbers by name. 34 | layertable = {} 35 | numlayers = pcbnew.PCB_LAYER_ID_COUNT 36 | for i in range(numlayers): 37 | layertable[pcbnew.GetBoard().GetLayerName(i)] = i 38 | 39 | 40 | def print_current_graphics(): 41 | # to get information about current graphics 42 | for d in board.GetDrawings(): 43 | if (d.Type() == pcbnew.PCB_LINE_T): 44 | # this type is DRAWSEGMENT in pcbnew/class_drawsegment.h 45 | # the different shape types are defined in class_board_item.h enum STROKE_T 46 | print("line shape {}".format(shape_table[d.GetShape()])) 47 | 48 | 49 | 50 | # this is sample code for adding a polygon. The downside of polygons is they are filled. 51 | # bummer 52 | # the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm) 53 | # the coordinate 121550000 corresponds to 121.550000 54 | # SCALE = 1000000.0 55 | # seg = pcbnew.DRAWSEGMENT(board) 56 | # seg.SetLayer(44) 57 | # seg.SetShape(pcbnew.S_POLYGON) 58 | # sps = seg.GetPolyShape() 59 | 60 | # o = sps.NewOutline() 61 | # sps.Append(int(10.0*SCALE),int(10.0*SCALE), o) 62 | # sps.Append(int(10.0*SCALE),int(20.0*SCALE), o) 63 | # sps.Append(int(20.0*SCALE),int(20.0*SCALE), o) 64 | # sps.Append(int(20.0*SCALE),int(10.0*SCALE), o) 65 | # board.Add(seg) 66 | 67 | 68 | 69 | 70 | def dxfarc2pcbarc(board, layer, center, radius, startangle, endangle): 71 | # dxf arcs are different from pcbnew arcs 72 | # dxf arcs have a center point, radius and start/stop angles 73 | # pcbnew arcs have a center pointer, radius, and a start point, angle (counter clockwise) 74 | 75 | seg = pcbnew.DRAWSEGMENT(board) 76 | seg.SetLayer(layer) 77 | seg.SetShape(pcbnew.S_ARC) 78 | seg.SetCenter(center.wxpoint()) 79 | # need negative angles because pcbnew flips over x axis 80 | sa, ea = (min(-startangle, -endangle), max(-startangle, -endangle)) 81 | 82 | seg.SetArcStart((center + pcbpoint.pcbpoint(radius*np.cos(np.deg2rad(startangle)), 83 | radius*np.sin(np.deg2rad(startangle)))).wxpoint()) 84 | # y axis is flipped, so negative angles 85 | seg.SetAngle((-endangle+startangle)*10) 86 | board.Add(seg) 87 | 88 | 89 | def dxf_to_graphic(board, layer, filepath, singlepoly=False): 90 | 91 | dxf = dxfgrabber.readfile(filepath) 92 | 93 | layer_count = len(dxf.layers) # collection of layer definitions 94 | block_definition_count = len(dxf.blocks) # dict like collection of block definitions 95 | entity_count = len(dxf.entities) # list like collection of entities 96 | print("layers: {}".format(layer_count)) 97 | print("blocks: {}".format(block_definition_count)) 98 | print("entities:{}".format(entity_count)) 99 | 100 | 101 | for e in dxf.entities.get_entities(): 102 | 103 | if (e.dxftype == "LINE"): 104 | seg = pcbnew.DRAWSEGMENT(board) 105 | seg.SetLayer(layer) 106 | seg.SetShape(pcbnew.S_SEGMENT) 107 | seg.SetStart(pcbpoint.pcbpoint(e.start).wxpoint()) 108 | seg.SetEnd(pcbpoint.pcbpoint(e.end).wxpoint()) 109 | board.Add(seg) 110 | 111 | if (e.dxftype == "CIRCLE"): 112 | print("center {} radius {}".format(e.center, e.radius)) 113 | 114 | if (e.dxftype == "ARC"): 115 | # dxf arcs are different from pcbnew arcs 116 | # dxf arcs have a center point, radius and start/stop angles 117 | # pcbnew arcs have a center pointer, radius, and a start point, 118 | # angle (counter clockwise) 119 | 120 | dxfarc2pcbarc(board, layer, 121 | pcbpoint.pcbpoint(e.center), 122 | e.radius, e.start_angle, e.end_angle) 123 | 124 | if (e.dxftype == "LWPOLYLINE"): 125 | if (singlepoly): 126 | seg = pcbnew.DRAWSEGMENT(board) 127 | seg.SetLayer(layer) 128 | seg.SetShape(pcbnew.S_POLYGON) 129 | board.Add(seg) 130 | sps = seg.GetPolyShape() 131 | 132 | o = sps.NewOutline() 133 | 134 | for pt in e.points: 135 | ppt = pcbpoint.pcbpoint(pt).wxpoint() 136 | sps.Append(ppt.x, ppt.y) 137 | 138 | else: 139 | 140 | prevpt = e.points[-1] 141 | curbulge = e.bulge[-1] 142 | for pt, nextbulge in zip(e.points, e.bulge): 143 | # y is minus because y increases going down the canvase 144 | if (curbulge == 0.0): 145 | seg = pcbnew.DRAWSEGMENT(board) 146 | seg.SetLayer(layer) 147 | seg.SetShape(pcbnew.S_SEGMENT) 148 | seg.SetStart(pcbpoint.pcbpoint(prevpt).wxpoint()) 149 | seg.SetEnd(pcbpoint.pcbpoint(pt).wxpoint()) 150 | board.Add(seg) 151 | else: 152 | center, startangle, endangle, radius = bulge.bulge2arc(prevpt, pt, curbulge) 153 | 154 | dxfarc2pcbarc(board, layer, 155 | pcbpoint.pcbpoint(center), 156 | radius, startangle, endangle) 157 | 158 | prevpt = pt 159 | curbulge = nextbulge 160 | 161 | 162 | 163 | 164 | pcbnew.Refresh() 165 | 166 | def dxf_to_mountholes(board,footprint_mapping, filepath): 167 | dxf = dxfgrabber.readfile(filepath) 168 | 169 | io = pcbnew.PCB_IO() 170 | for e in dxf.entities.get_entities(): 171 | 172 | if (e.dxftype == "CIRCLE"): 173 | print("center {} radius {}".format(e.center, e.radius)) 174 | d = str(e.radius*2) 175 | if d not in footprint_mapping: 176 | raise ValueError("diameter {} not found in footprint mapping".format(d)) 177 | fp = footprint_mapping[d] 178 | mod = io.FootprintLoad(fp[0], fp[1]) 179 | mod.SetPosition(pcbpoint.pcbpoint(e.center).wxpoint()) 180 | board.Add(mod) 181 | pcbnew.Refresh() 182 | 183 | 184 | board = pcbnew.GetBoard() 185 | 186 | dxf_to_graphic(board, layertable['Cmts.User'], 187 | "/bubba/electronicsDS/fusion/leds_projection.dxf", True) 188 | 189 | dxf_to_graphic(board, layertable['Edge.Cuts'], 190 | "/bubba/electronicsDS/fusion/boundary_polyline.dxf") 191 | 192 | footprint_lib = '/home/mmccoo/kicad/kicad-footprints/MountingHole.pretty' 193 | 194 | footprint_mapping = { 195 | "3.0": (footprint_lib, "MountingHole_3.2mm_M3") 196 | } 197 | dxf_to_mountholes(board, footprint_mapping, "/bubba/electronicsDS/fusion/mountingholes.dxf") 198 | 199 | 200 | dxf_to_graphic(board, layertable['Eco1.User'], 201 | "/bubba/electronicsDS/fusion/powerrails.dxf") 202 | 203 | #traverse_dxf("/bubba/electronicsDS/fusion/powerrails.dxf", graphic_actions) 204 | -------------------------------------------------------------------------------- /dxf_stuff/dxf_utils.py: -------------------------------------------------------------------------------- 1 | 2 | import dxfgrabber 3 | import numpy as np 4 | import sys, os.path, inspect 5 | import re 6 | import pcbnew 7 | import bulge 8 | import pcbpoint 9 | 10 | from sets import Set 11 | import pdb 12 | 13 | # how close together to points need to be to each other to be considered 14 | # connected? 15 | thresh = 0.01 16 | # when breaking and arc into segments, what's the max seg length we want. 17 | arc_line_length = 5.0 18 | 19 | # I mostly depend on pcbpoint to deal with scaling issues. For radius below, 20 | # still need to scale. 21 | # the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm) 22 | # the coordinate 121550000 corresponds to 121.550000 23 | SCALE = 1000000.0 24 | 25 | 26 | 27 | class graphic_actions: 28 | def __init__(self, print_unhandled=False): 29 | self.print_unhandled = print_unhandled 30 | 31 | def line_action(self, start, end): 32 | if (self.print_unhandled): 33 | print("line: {} {}".format(start, end)) 34 | 35 | def circle_action(self, center, radius): 36 | if (self.print_unhandled): 37 | print("circle center: {} radius: {}".format(center, radius)) 38 | 39 | def arc_action(self, center, radius, start_angle, end_angle): 40 | if (self.print_unhandled): 41 | print("arc center: {} radius {} angles: {} {}".format(center, radius, start_angle, end_angle)) 42 | 43 | def poly_action(self, points): 44 | if (self.print_unhandled): 45 | print("poly: {}".format(points)) 46 | 47 | # here is some helper stuff 48 | # dxf arcs are different from pcbnew arcs 49 | # dxf arcs have a center point, radius and start/stop angles 50 | # pcbnew arcs have a center pointer, radius, and a start point, 51 | # angle (counter clockwise) 52 | def dxfarc2pcbarc(self, center, radius, start_angle, end_angle): 53 | # need negative angles because pcbnew flips over x axis 54 | start_angle, end_angle = (min(start_angle, end_angle), max(start_angle, end_angle)) 55 | return (center, 56 | start_angle-end_angle, 57 | # location of start of arc 58 | center.polar(radius, start_angle)) 59 | 60 | 61 | class segment_actions(graphic_actions): 62 | def __init__(self, board, layer, print_unhandled=False): 63 | graphic_actions.__init__(self, print_unhandled) 64 | 65 | self.board = board 66 | self.layer = layer 67 | 68 | def make_basic_seg(self): 69 | seg = pcbnew.DRAWSEGMENT(self.board) 70 | seg.SetLayer(self.layer) 71 | seg.SetShape(pcbnew.S_SEGMENT) 72 | self.board.Add(seg) 73 | return seg 74 | 75 | def line_action(self, start, end): 76 | seg = self.make_basic_seg() 77 | seg.SetShape(pcbnew.S_SEGMENT) 78 | seg.SetStart(pcbpoint.pcbpoint(start).wxpoint()) 79 | seg.SetEnd(pcbpoint.pcbpoint(end).wxpoint()) 80 | 81 | def circle_action(self, center, radius): 82 | seg = self.make_basic_seg() 83 | seg.SetShape(pcbnew.S_CIRCLE) 84 | seg.SetCenter(pcbpoint.pcbpoint(center).wxpoint()) 85 | # kicad has a goofy way of specifying circles. instead 86 | # of a radius, you give a point on the circle. The radius 87 | # can be computed from there. 88 | seg.SetEnd((pcbpoint.pcbpoint(center)+ 89 | pcbpoint.pcbpoint(radius, 0)).wxpoint() 90 | ) 91 | 92 | def arc_action(self, center, radius, start_angle, end_angle): 93 | # dxf arcs are different from pcbnew arcs 94 | # dxf arcs have a center point, radius and start/stop angles 95 | # pcbnew arcs have a center pointer, radius, and a start point, 96 | # angle (counter clockwise) 97 | seg = self.make_basic_seg() 98 | 99 | center, angle, arcstart = self.dxfarc2pcbarc(pcbpoint.pcbpoint(center), 100 | radius, 101 | start_angle, 102 | end_angle) 103 | seg.SetShape(pcbnew.S_ARC) 104 | seg.SetCenter(pcbpoint.pcbpoint(center).wxpoint()) 105 | # negative angle since y goes the wrong way. 106 | seg.SetAngle(angle*10) 107 | seg.SetArcStart(pcbpoint.pcbpoint(arcstart).wxpoint()) 108 | 109 | def poly_action(self, points): 110 | seg = self.make_basic_seg() 111 | seg.SetShape(pcbnew.S_POLYGON) 112 | 113 | sps = seg.GetPolyShape() 114 | o = sps.NewOutline() 115 | for pt in points: 116 | ppt = pcbpoint.pcbpoint(pt).wxpoint() 117 | sps.Append(ppt.x, ppt.y) 118 | 119 | class zone_actions(graphic_actions): 120 | def __init__(self, board, net, layer, print_unhandled=False): 121 | graphic_actions.__init__(self, print_unhandled) 122 | self.board = board 123 | self.net = net 124 | self.layer = layer 125 | 126 | # poly is the only thing that really makes sense in 127 | # the zone context 128 | def poly_action(self, points): 129 | pcbpt = pcbpoint.pcbpoint(points[0]).wxpoint() 130 | zone_container = self.board.InsertArea(self.net.GetNet(), 0, self.layer, 131 | pcbpt.x, pcbpt.y, 132 | pcbnew.CPolyLine.DIAGONAL_EDGE) 133 | shape_poly_set = zone_container.Outline() 134 | shapeid = 0 135 | for pt in points[1:]: 136 | pcbpt = pcbpoint.pcbpoint(pt).wxpoint() 137 | shape_poly_set.Append(pcbpt.x, pcbpt.y) 138 | 139 | zone_container.Hatch() 140 | 141 | 142 | class mounting_actions(graphic_actions): 143 | 144 | def __init__(self, board, footprint_mapping, 145 | flip=False, 146 | clearance=None, 147 | print_unhandled=False): 148 | graphic_actions.__init__(self, print_unhandled) 149 | 150 | self.footprint_mapping = footprint_mapping 151 | self.board = board 152 | self.flip = flip 153 | self.clearance = clearance 154 | 155 | def circle_action(self, center, radius): 156 | d = str(radius*2) 157 | if d not in self.footprint_mapping: 158 | print("diameter {} not found in footprint mapping".format(d)) 159 | return 160 | fp = self.footprint_mapping[d] 161 | mod = pcbnew.InstantiateFootprint(fp[0], fp[1]) 162 | mod.SetPosition(pcbpoint.pcbpoint(center).wxpoint()) 163 | if (self.flip): 164 | mod.Flip(pcbpoint.pcbpoint(center).wxpoint()) 165 | if (self.clearance != None): 166 | for pad in mod.Pads(): 167 | pad.SetLocalClearance(self.clearance) 168 | self.board.Add(mod) 169 | 170 | 171 | # http://www.ariel.com.au/a/python-point-int-poly.html 172 | # determine if a point is inside a given polygon or not 173 | # Polygon is a list of (x,y) pairs. 174 | def point_inside_polygon(x,y,poly): 175 | 176 | n = len(poly) 177 | inside =False 178 | 179 | p1x,p1y = poly[0] 180 | for i in range(n+1): 181 | p2x,p2y = poly[i % n] 182 | if y > min(p1y,p2y): 183 | if y <= max(p1y,p2y): 184 | if x <= max(p1x,p2x): 185 | if p1y != p2y: 186 | xinters = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x 187 | if p1x == p2x or x <= xinters: 188 | inside = not inside 189 | p1x,p1y = p2x,p2y 190 | 191 | return inside 192 | 193 | 194 | def longest_angle_for_polygon(poly): 195 | prevpt = poly[-1] 196 | length = None 197 | retval = None 198 | for pt in poly: 199 | d = prevpt.distance(pt) 200 | if (length and (length>d)): 201 | prevpt = pt 202 | continue 203 | length = d 204 | retval = prevpt.angle(pt) 205 | prevpt = pt 206 | return retval 207 | 208 | 209 | def center_for_polygon(poly): 210 | # based on this: 211 | # https://en.wikipedia.org/wiki/Centroid#Centroid_of_a_polygon 212 | 213 | prev = poly[-1] 214 | x = 0.0 215 | y = 0.0 216 | area = 0.0 217 | for cur in poly: 218 | x = x + (prev.x+cur.x)*(prev.x*cur.y - cur.x*prev.y) 219 | y = y + (prev.y+cur.y)*(prev.x*cur.y - cur.x*prev.y) 220 | area = area + prev.x*cur.y - cur.x*prev.y 221 | prev = cur 222 | 223 | area = area/2.0 224 | 225 | x = x/6.0/area 226 | y = y/6.0/area 227 | 228 | return pcbpoint.pcbpoint(x,y,noscale=True) 229 | 230 | class orient_actions(graphic_actions): 231 | def __init__(self, board, modnames, print_unhandled=False): 232 | graphic_actions.__init__(self, print_unhandled) 233 | self.board = board 234 | self.modnames = Set(modnames) 235 | 236 | # I only care about poly because I want directionality (which a cirle doesn't have) 237 | # and I want to check for enclosing (which doesn't make sense for line, arc 238 | def poly_action(self, points): 239 | for mod in self.board.GetModules(): 240 | #modname = mod.GetFPID().GetLibItemName().c_str() 241 | #if (modname != "LED_5730"): 242 | # continue 243 | modname = mod.GetReference() 244 | if (modname not in self.modnames): 245 | continue 246 | pos = pcbpoint.pcbpoint(mod.GetPosition()) 247 | inside = point_inside_polygon(pos.x, pos.y, points) 248 | if (not inside): 249 | continue 250 | angle = longest_angle_for_polygon(points) 251 | if (angle>0): 252 | angle = angle - 180.0 253 | mod.SetOrientation(angle*10) 254 | mod.SetPosition(center_for_polygon(points).wxpoint()) 255 | 256 | class myarc: 257 | def __init__(self, center, radius, start_angle, end_angle): 258 | self.center = center = pcbpoint.pcbpoint(center) 259 | self.radius = radius 260 | self.start_angle = start_angle 261 | self.end_angle = end_angle 262 | 263 | self.start_point = center.polar(radius, start_angle) 264 | self.end_point = center.polar(radius, end_angle) 265 | self.other = Set() 266 | 267 | def reverse(self): 268 | self.start_angle, self.end_angle = (self.end_angle, self.start_angle) 269 | self.start_point, self.end_point = (self.end_point, self.start_point) 270 | 271 | def __str__(self): 272 | return "arc c{} r{} {},{} {},{}".format(self.center, self.radius, self.start_angle, self.end_angle, self.start_point, self.end_point) 273 | 274 | class myline: 275 | def __init__(self, start_point, end_point): 276 | self.start_point = pcbpoint.pcbpoint(start_point) 277 | self.end_point = pcbpoint.pcbpoint(end_point) 278 | self.other = Set() 279 | 280 | def reverse(self): 281 | self.start_point, self.end_point = (self.end_point, self.start_point) 282 | 283 | def __str__(self): 284 | return "line {} {}".format(self.start_point, self.end_point) 285 | 286 | 287 | def mydist(o1, o2): 288 | return min(o1.start_point.distance(o2.start_point), 289 | o1.start_point.distance(o2.end_point), 290 | o1.end_point.distance(o2.start_point), 291 | o1.end_point.distance(o2.end_point)) 292 | 293 | 294 | # it is possible for two polygons to meet at a point. This implementation 295 | # will destroy those. worry about it later. 296 | def remove_non_duals(e): 297 | if (len(e.other) == 2): 298 | return 299 | 300 | # if I have three lines joining in one spot. this doesn't deal 301 | # with the properly. I try to mitigate that by beginning with 302 | # lines that connect at only one edge first. 303 | others = e.other 304 | e.other = Set() 305 | for other in others: 306 | other.other.remove(e) 307 | remove_non_duals(other) 308 | 309 | def merge_arcs_and_lines(elts): 310 | 311 | 312 | # yes, this is a O(n^2) algorithm. scanline would be better 313 | # this is quicker to implement 314 | for e1 in elts: 315 | for e2 in elts: 316 | if (e1==e2): 317 | continue 318 | if mydist(e1, e2) thresh) and 361 | (members[0].end_point.distance(prev.end_point) > thresh)): 362 | prev.reverse() 363 | 364 | for m in members: 365 | if (m.start_point.distance(prev.end_point) > thresh): 366 | m.reverse() 367 | if (m.start_point.distance(prev.end_point) > thresh): 368 | raise ValueError("expecting the start and end to match here {} {}".format(prev, m)) 369 | 370 | prev = m 371 | 372 | merged.append(members) 373 | 374 | return merged 375 | 376 | def break_curve(center, radius, start_angle, end_angle): 377 | retpts = [] 378 | 379 | center = pcbpoint.pcbpoint(center) 380 | 381 | # in this file, I generally use degrees (kicad uses degrees), 382 | # in this function, radians more convenient. 383 | start_radians, end_radians = (np.deg2rad(start_angle), np.deg2rad(end_angle)) 384 | 385 | # the circumference of a cirle is 2*pi*radius 386 | circ = np.abs(end_radians-start_radians)*radius 387 | 388 | num_segs = int(np.max((np.ceil(circ/arc_line_length), 389 | np.ceil(np.abs(end_angle-start_angle)/15.0)))) 390 | incr_radians = (end_radians-start_radians)/num_segs 391 | 392 | for i in range(num_segs+1): 393 | radians = start_radians + incr_radians*i 394 | retpts.append(center.polar(radius, np.rad2deg(radians))) 395 | 396 | return retpts 397 | 398 | # unlike the other functions where I just pass generic attributes, (center, radius...) 399 | # since bulge is specific to dxf polylines, I'm just passing the polyline 400 | def break_bulges(e): 401 | retpts = [] 402 | 403 | prevpt = e.points[-1] 404 | curbulge = e.bulge[-1] 405 | for pt, nextbulge in zip(e.points, e.bulge): 406 | if (curbulge == 0.0): 407 | retpts.append(pt) 408 | prevpt = pt 409 | curbulge = nextbulge 410 | continue 411 | 412 | # dxf arcs are different from pcbnew arcs 413 | # dxf arcs have a center point, radius and start/stop angles 414 | # pcbnew arcs have a center pointer, radius, and a start point, angle (counter clockwise) 415 | 416 | # the angles are negative because pcbnew coodinates are flipped over the x axis 417 | center, start_angle, end_angle, radius = bulge.bulge2arc(prevpt, pt, curbulge) 418 | 419 | arcpts = break_curve(center, radius, start_angle, end_angle) 420 | # remove the first point because we don't want repeats in this poly 421 | arcpts.pop(0) 422 | retpts.extend(arcpts) 423 | 424 | prevpt = pt 425 | curbulge = nextbulge 426 | 427 | return retpts 428 | 429 | def traverse_dxf(filepath, actions, 430 | merge_polys=False, 431 | break_curves=False): 432 | dxf = dxfgrabber.readfile(filepath) 433 | 434 | merge_elts = [] 435 | for e in dxf.entities.get_entities(): 436 | if (e.dxftype == "LINE"): 437 | if (merge_polys): 438 | merge_elts.append(myline(e.start, e.end)) 439 | else: 440 | actions.line_action(pcbpoint.pcbpoint(e.start), 441 | pcbpoint.pcbpoint(e.end)) 442 | 443 | elif(e.dxftype == "CIRCLE"): 444 | actions.circle_action(pcbpoint.pcbpoint(e.center), e.radius) 445 | 446 | elif(e.dxftype == "ARC"): 447 | if (merge_polys): 448 | merge_elts.append(myarc(e.center, e.radius, e.start_angle, e.end_angle)) 449 | continue 450 | 451 | if (not break_curves): 452 | actions.arc_action(pcbpoint.pcbpoint(e.center), e.radius, e.start_angle, e.end_angle) 453 | continue 454 | 455 | pts = break_curve(e.center, e.radius, e.start_angle, e.end_angle) 456 | prevpt = pts.pop(0) 457 | for pt in pts: 458 | actions.line_action(prevpt, pt) 459 | prevpt = pt 460 | 461 | elif (e.dxftype == "LWPOLYLINE"): 462 | pts = e.points 463 | if (break_curves): 464 | pts = break_bulges(e) 465 | if (merge_polys): 466 | # if we're asking polygons to be merged, we just leave 467 | # existing polys as they are. 468 | actions.poly_action([pcbpoint.pcbpoint(p) for p in pts]) 469 | else: 470 | # otherwise, I will un-merge polys 471 | prevpt = pcbpoint.pcbpoint(pts[-1]) 472 | for p in pts: 473 | curpt = pcbpoint.pcbpoint(p) 474 | actions.line_action(prevpt, curpt) 475 | prevpt = curpt 476 | 477 | 478 | if (not merge_polys): 479 | return 480 | 481 | merged = merge_arcs_and_lines(merge_elts) 482 | 483 | # at this point, if there were objects that weren't merged into something, they'll 484 | # be lost. 485 | 486 | for poly in merged: 487 | pts = [] 488 | for elt in poly: 489 | # when talking about polys in kicad, there are no arcs. 490 | # so I'm just assuming that either, the arcs have been broken 491 | # in the code above, or that the user doesn't care about the lost 492 | # accuracy 493 | if (break_curves and isinstance(elt, myarc)): 494 | brokenpts = break_curve(elt.center, elt.radius, elt.start_angle, elt.end_angle) 495 | # pop the last point because the next elt will give that point. 496 | # adjacent element share a start/end point. We only want it once. 497 | brokenpts.pop() 498 | pts.extend(brokenpts) 499 | else: 500 | pts.append(elt.start_point) 501 | actions.poly_action(pts) 502 | 503 | 504 | def traverse_graphics(board, layer, actions, 505 | merge_polys=False, 506 | break_curves=False): 507 | 508 | merge_elts = [] 509 | for d in board.GetDrawings(): 510 | if ((layer != None) and (layer != d.GetLayerName())): 511 | continue 512 | 513 | if (d.GetShape() == pcbnew.S_SEGMENT): 514 | if (merge_polys): 515 | merge_elts.append(myline(d.GetStart(), d.GetEnd())) 516 | else: 517 | actions.line_action(d.GetStart(), d.GetEnd()) 518 | 519 | elif (d.GetShape() == pcbnew.S_CIRCLE): 520 | actions.circle_action(d.GetCenter(), d.GetRadius()/SCALE) 521 | 522 | elif(d.GetShape() == pcbnew.S_ARC): 523 | if (merge_polys): 524 | merge_elts.append(myarc(d.GetCenter(), 525 | d.GetRadius()/SCALE, 526 | -d.GetArcAngleStart()/10.0, 527 | -(d.GetArcAngleStart()+d.GetAngle())/10.0)) 528 | continue 529 | 530 | if (not break_curves): 531 | # negative angles because kicad's y axis goes down. 532 | actions.arc_action(d.GetCenter(), 533 | d.GetRadius()/SCALE, 534 | -d.GetArcAngleStart()/10.0, 535 | -(d.GetArcAngleStart()+d.GetAngle())/10.0) 536 | continue 537 | 538 | pts = break_curve(d.GetCenter(), 539 | d.GetRadius()/SCALE, 540 | -d.GetArcAngleStart()/10.0, 541 | -(d.GetArcAngleStart()+d.GetAngle())/10.0) 542 | prevpt = pts.pop(0) 543 | for pt in pts: 544 | actions.line_action(prevpt, pt) 545 | prevpt = pt 546 | 547 | elif (d.GetShape() == pcbnew.S_POLYGON): 548 | pts = [pcbpoint.pcbpoint(p) for p in d.GetPolyPoints()] 549 | actions.poly_action(pts) 550 | 551 | if (not merge_polys): 552 | return 553 | 554 | merged = merge_arcs_and_lines(merge_elts) 555 | 556 | # at this point, if there were objects that weren't merged into something, they'll 557 | # be lost. 558 | 559 | for poly in merged: 560 | pts = [] 561 | for elt in poly: 562 | # when talking about polys in kicad, there are no arcs. 563 | # so I'm just assuming that either, the arcs have been broken 564 | # in the code above, or that the user doesn't care about the lost 565 | # accuracy 566 | if (break_curves and isinstance(elt, myarc)): 567 | brokenpts = break_curve(elt.center, elt.radius, elt.start_angle, elt.end_angle) 568 | # pop the last point because the next elt will give that point. 569 | # adjacent element share a start/end point. We only want it once. 570 | brokenpts.pop() 571 | pts.extend(brokenpts) 572 | else: 573 | pts.append(elt.start_point) 574 | actions.poly_action(pts) 575 | -------------------------------------------------------------------------------- /dxf_stuff/mounting.py: -------------------------------------------------------------------------------- 1 | import wx 2 | import wx.lib 3 | import wx.grid 4 | import pcbnew 5 | import ntpath 6 | import pdb 7 | import sys, os.path, inspect 8 | from sets import Set 9 | 10 | from ..save_config import save_config 11 | from ..simpledialog import DialogUtils 12 | 13 | # oldpath = sys.path 14 | # # inspect.stack()[0][1] is the full path to the current file. 15 | # sys.path.insert(0, os.path.dirname(inspect.stack()[0][1])) 16 | # import DialogUtils 17 | # import save_config 18 | # sys.path = oldpath 19 | 20 | 21 | class MountingDialog(DialogUtils.BaseDialog): 22 | def __init__(self, configname=None): 23 | super(MountingDialog, self).__init__("mounting hole dialog", onok=self.OnOKCB) 24 | 25 | homedir = os.path.expanduser("~") 26 | self.file_picker = DialogUtils.FilePicker(self, homedir, 27 | wildcard="DXF files (.dxf)|*.dxf", 28 | configname="mountingdialog") 29 | self.AddLabeled(item=self.file_picker, label="DXF file", 30 | proportion=0, flag=wx.EXPAND|wx.ALL, border=2) 31 | 32 | self.grid = wx.Panel(self) 33 | self.gridSizer = wx.FlexGridSizer(cols=4, hgap=5, vgap=0) 34 | self.grid.SetSizer(self.gridSizer) 35 | 36 | self.configname = configname 37 | 38 | self.therows = {} 39 | 40 | self.mappings = [] 41 | if self.configname != None: 42 | self.mappings = save_config.GetConfigComplex(self.configname, []) 43 | 44 | for size in self.mappings: 45 | lib,foot = self.mappings[size] 46 | self.AddOption(size, lib, foot) 47 | 48 | self.AddLabeled(self.grid, "diameter to footprint mappings", 49 | proportion=1, 50 | flag=wx.EXPAND|wx.ALL, 51 | border=0) 52 | 53 | 54 | w = wx.Window(self) 55 | s = wx.BoxSizer(wx.HORIZONTAL) 56 | w.SetSizer(s) 57 | 58 | self.flip = wx.CheckBox(w, label="Flip to backside") 59 | s.Add(self.flip) 60 | 61 | self.add = wx.Button(w, label="Add Row") 62 | self.add.Bind(wx.EVT_BUTTON, self.OnAdd) 63 | s.Add(self.add, proportion=1) 64 | 65 | 66 | self.Add(w, flag=wx.EXPAND|wx.ALL, border=0) 67 | 68 | 69 | #self.IncSize(width=25, height=10) 70 | self.Fit() 71 | 72 | def AddOption(self, size, lib, foot): 73 | 74 | s = wx.SpinCtrlDouble(self.grid, value=str(size), inc=0.1) 75 | self.gridSizer.Add(s) 76 | 77 | print("lib {} foot {}".format(lib, foot)) 78 | l = wx.StaticText(self.grid, label=lib) 79 | self.gridSizer.Add(l, proportion=1) 80 | 81 | f = wx.StaticText(self.grid, label=foot) 82 | self.gridSizer.Add(f, proportion=1) 83 | 84 | b = wx.Button(self.grid, label="remove") 85 | self.gridSizer.Add(b) 86 | b.Bind(wx.EVT_BUTTON, self.OnRemove) 87 | 88 | self.therows[b.GetId()] = (s,l,f,b) 89 | 90 | def OnAdd(self, event): 91 | dlg = DialogUtils.FootprintDialog() 92 | res = dlg.ShowModal() 93 | if (res != wx.ID_OK): 94 | return 95 | 96 | self.AddOption(1, dlg.libpicker.value, dlg.modpicker.value) 97 | self.Fit() 98 | 99 | 100 | def OnRemove(self, event): 101 | id = event.EventObject.GetId() 102 | wins = self.therows[id] 103 | del self.therows[id] 104 | wins[0].Destroy() 105 | wins[1].Destroy() 106 | wins[2].Destroy() 107 | wins[3].Destroy() 108 | 109 | self.Fit() 110 | 111 | def SetValue(self): 112 | self.value = {} 113 | for id in self.therows: 114 | row = self.therows[id] 115 | size = row[0].GetValue() 116 | lib = row[1].GetLabel() 117 | foot = row[2].GetLabel() 118 | self.value[str(size)] = (lib, foot) 119 | 120 | def OnOKCB(self): 121 | self.SetValue() 122 | if (self.configname != None): 123 | save_config.SaveConfigComplex(self.configname, self.value) 124 | 125 | 126 | 127 | # dlg = MountingDialog(configname = "mountingmap") 128 | # res = dlg.ShowModal() 129 | 130 | # print("lib {} footprint {}".format(dlg.value)) 131 | 132 | # print("nets {}".format(dlg.nets.value)) 133 | # print("mods {}".format(dlg.mods.value)) 134 | # #print("file {}".format(dlg.file_picker.filename)) 135 | # #print("basic layer {}".format(dlg.basic_layer.value)) 136 | # if res == wx.ID_OK: 137 | # print("ok") 138 | # else: 139 | # print("cancel") 140 | -------------------------------------------------------------------------------- /dxf_stuff/orient_to_polys.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import pcbnew 4 | import re 5 | import math 6 | import numpy 7 | 8 | # http://www.ariel.com.au/a/python-point-int-poly.html 9 | # determine if a point is inside a given polygon or not 10 | # Polygon is a list of (x,y) pairs. 11 | def point_inside_polygon(x,y,poly): 12 | 13 | n = len(poly) 14 | inside =False 15 | 16 | p1x,p1y = poly[0] 17 | for i in range(n+1): 18 | p2x,p2y = poly[i % n] 19 | if y > min(p1y,p2y): 20 | if y <= max(p1y,p2y): 21 | if x <= max(p1x,p2x): 22 | if p1y != p2y: 23 | xinters = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x 24 | if p1x == p2x or x <= xinters: 25 | inside = not inside 26 | p1x,p1y = p2x,p2y 27 | 28 | return inside 29 | 30 | def distpts(a,b): 31 | return numpy.sqrt((a[0]-b[0])**2+(a[1]-b[1])**2) 32 | 33 | def anglepts(a,b): 34 | return math.degrees(math.atan2(b[1]-a[1], b[0]-a[0])) 35 | 36 | def longest_angle_for_polygon(poly): 37 | prevpt = poly[-1] 38 | length = None 39 | retval = None 40 | for pt in poly: 41 | d = distpts(prevpt, pt) 42 | if (length and (length>d)): 43 | prevpt = pt 44 | continue 45 | length = d 46 | retval = anglepts(prevpt, pt) 47 | prevpt = pt 48 | return retval 49 | 50 | 51 | board = pcbnew.GetBoard() 52 | 53 | type_table = {} 54 | for t in filter(lambda t: re.match("PCB_.*_T", t), dir(pcbnew)): 55 | type_table[getattr(pcbnew, t)] = t 56 | shape_table = {} 57 | for s in filter(lambda s: re.match("S_.*", s), dir(pcbnew)): 58 | shape_table[getattr(pcbnew, s)] = s 59 | 60 | #for d in board.GetDrawings(): 61 | # print("type {} {} {}".format(type_table[d.Type()], 62 | # shape_table[d.GetShape()], 63 | # d.GetLayerName())) 64 | for mod in board.GetModules(): 65 | # this should be exposed better. 66 | modname = mod.GetFPID().GetLibItemName().c_str() 67 | if (modname != "LED_5730"): 68 | continue 69 | pos = mod.GetPosition() 70 | #print("mod {}".format(mod.GetReference())) 71 | for d in board.GetDrawings(): 72 | if (d.GetLayerName() != 'Cmts.User'): 73 | continue 74 | bbox = d.GetBoundingBox() 75 | # this is just a rough check. the bbox can be much bigger then 76 | # the polygon is wraps around. 77 | if (not bbox.Contains(pos)): 78 | continue 79 | pts = [(pt.x, pt.y) for pt in d.GetPolyPoints()] 80 | inside = point_inside_polygon(pos.x, pos.y, pts) 81 | if (not inside): 82 | continue 83 | # negative angle because y goes down. 84 | angle = -longest_angle_for_polygon(pts) 85 | mod.SetOrientation(angle*10) 86 | mod.SetPosition(d.GetCenter()) 87 | print("mod {} d {} {} {} {}".format(mod.GetReference(), 88 | bbox.GetPosition(), 89 | bbox.GetWidth(), 90 | bbox.GetHeight(), 91 | angle 92 | )) 93 | pcbnew.Refresh() 94 | 95 | -------------------------------------------------------------------------------- /dxf_stuff/pcbpoint.py: -------------------------------------------------------------------------------- 1 | 2 | import pcbnew 3 | import pdb 4 | import math 5 | 6 | # kicad, like many programs uses a coordinate space where y=0 at the top and increases 7 | # down. This is the reverse of what I was taught in high school math class. 8 | # dxf file use the "normal" euclidean space. 9 | # also, need to set the x,y origin to some place within the pcbnew canvas. 10 | 11 | 12 | 13 | class pcbpoint: 14 | origin = (50,150) 15 | # the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm) 16 | # the coordinate 121550000 corresponds to 121.550000 17 | SCALE = 1000000.0 18 | 19 | def __init__(self, x=0.0, y=0.0, noscale=False): 20 | if (noscale): 21 | scale = 1 22 | else: 23 | scale = self.SCALE 24 | 25 | if (isinstance(x, pcbpoint)): 26 | self.x = x.x 27 | self.y = x.y 28 | elif (isinstance(x, pcbnew.wxPoint)): 29 | # later, when I get a wxpoint, I'll put the origin back 30 | self.x = x.x - self.origin[0]*self.SCALE 31 | self.y = self.origin[1]*self.SCALE - x.y 32 | elif (type(x) == tuple): 33 | self.x = int(scale*x[0]) 34 | self.y = int(scale*x[1]) 35 | elif (type(x) == list): 36 | pdb.set_trace() 37 | else: 38 | self.x = int(scale*x) 39 | self.y = int(scale*y) 40 | 41 | 42 | def wxpoint(self): 43 | # y is minus because y increases going down the canvase 44 | return pcbnew.wxPoint(self.origin[0]*self.SCALE+self.x, 45 | self.origin[1]*self.SCALE-self.y) 46 | 47 | def polar(self, radius, angle): 48 | return pcbpoint(self.x + self.SCALE*radius*math.cos(math.radians(angle)), 49 | self.y + self.SCALE*radius*math.sin(math.radians(angle)), 50 | noscale=True 51 | ) 52 | 53 | def angle(self, other): 54 | return math.degrees(math.atan2(other.y-self.y, other.x-self.x)) 55 | 56 | def __add__(self, other): 57 | return pcbpoint(self.x+other.x, self.y+other.y, noscale=True) 58 | 59 | def __str__(self): 60 | return "({},{})".format(self.x/self.SCALE, self.y/self.SCALE) 61 | 62 | def __iter__(self): 63 | for i in range(2): 64 | if (i == 0): 65 | yield self.x 66 | elif (i == 1): 67 | yield self.y 68 | else: 69 | raise KeyError 70 | 71 | def distance(self, other): 72 | return math.sqrt(float(self.x-other.x)**2+ 73 | float(self.y-other.y)**2)/self.SCALE 74 | 75 | # p1 = pcbpoint(10,10) 76 | # print("double {}".format(p1+p1)) 77 | # print("double {}".format((p1+p1).wxpoint())) 78 | 79 | # pt = (42,77) 80 | # p2 = pcbpoint(pt) 81 | # print("double {}".format(p1+p2)) 82 | # print("double {}".format((p1+p2).wxpoint())) 83 | -------------------------------------------------------------------------------- /dxf_stuff/recipe.py: -------------------------------------------------------------------------------- 1 | 2 | import sys,inspect,os 3 | oldpath = sys.path 4 | # inspect.stack()[0][1] is the full path to the current file. 5 | sys.path.insert(0, os.path.dirname(inspect.stack()[0][1])) 6 | import bulge 7 | import pcbpoint 8 | sys.path = oldpath 9 | 10 | from dxf_utils import traverse_dxf 11 | from dxf_utils import traverse_graphics 12 | from dxf_utils import segment_actions 13 | from dxf_utils import mounting_actions 14 | from dxf_utils import orient_actions 15 | from dxf_utils import zone_actions 16 | 17 | 18 | import pcbnew 19 | import re 20 | 21 | 22 | #graphic_actions has callback to just print what's there 23 | #traverse_dxf("/bubba/electronicsDS/fusion/powerrails.dxf", graphic_actions(True)) 24 | 25 | # segment actions has callbacks to create graphic polys 26 | board = pcbnew.GetBoard() 27 | # generate a name->layer table so we can lookup layer numbers by name. 28 | layertable = {} 29 | numlayers = pcbnew.PCB_LAYER_ID_COUNT 30 | for i in range(numlayers): 31 | layertable[pcbnew.GetBoard().GetLayerName(i)] = i 32 | 33 | shape_table = {} 34 | for s in filter(lambda s: re.match("S_.*", s), dir(pcbnew)): 35 | shape_table[getattr(pcbnew, s)] = s 36 | 37 | 38 | 39 | #traverse_dxf("/bubba/electronicsDS/fusion/powerrails.dxf", 40 | # segment_actions(board, layertable['Eco1.User'])) 41 | #traverse_dxf("/bubba/electronicsDS/fusion/powerrails.dxf", 42 | # graphic_actions(True), 43 | # merge_polys=True) 44 | 45 | 46 | if (0): 47 | traverse_dxf("/bubba/electronicsDS/fusion/leds_projection.dxf", 48 | segment_actions(board, layertable['Cmts.User']), 49 | merge_polys=True, 50 | break_curves=True) 51 | 52 | 53 | if (0): 54 | if (1): 55 | traverse_graphics(board, 'Cmts.User', 56 | orient_actions(board, "LED_5730"), 57 | merge_polys=True, 58 | break_curves=True) 59 | else: 60 | traverse_dxf("/bubba/electronicsDS/fusion/leds_projection.dxf", 61 | orient_actions(board, "LED_5730"), 62 | merge_polys=True, 63 | break_curves=True) 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | powerlayer = layertable["B.Cu"] 72 | # find a power net to add the zone to. 73 | powernet = None 74 | nets = board.GetNetsByName() 75 | for name in ["+12V", "+5V", "GND"]: 76 | if (nets.has_key(name)): 77 | powernet = nets[name] 78 | 79 | if (0): 80 | traverse_dxf("/bubba/electronicsDS/fusion/powerrails.dxf", 81 | zone_actions(board, powernet, layertable["B.Cu"]), 82 | merge_polys=True, 83 | break_curves=True 84 | ) 85 | 86 | if (0): 87 | traverse_dxf("/bubba/electronicsDS/fusion/cross_rails.dxf", 88 | zone_actions(board, powernet, layertable["F.Cu"]), 89 | merge_polys=True, 90 | break_curves=True 91 | ) 92 | 93 | if (0): 94 | traverse_dxf("/bubba/electronicsDS/fusion/powerrails.dxf", 95 | segment_actions(board, layertable['Eco1.User']), 96 | merge_polys=True, 97 | break_curves=True 98 | ) 99 | 100 | 101 | if (0): 102 | traverse_dxf("/bubba/electronicsDS/fusion/boundary.dxf", 103 | segment_actions(board, layertable['Edge.Cuts']), 104 | merge_polys=False, 105 | break_curves=True) 106 | 107 | footprint_lib = '/home/mmccoo/kicad/kicad-footprints/MountingHole.pretty' 108 | testpad_lib = '/home/mmccoo/kicad/kicad-footprints/TestPoint.pretty/' 109 | footprint_mapping = { 110 | "3.0": (footprint_lib, "MountingHole_3.2mm_M3") 111 | } 112 | 113 | if (1): 114 | traverse_dxf("/bubba/electronicsDS/fusion/mountingholes.dxf", 115 | mounting_actions(board, 116 | footprint_mapping, 117 | clearance=pcbnew.Millimeter2iu(1) 118 | )) 119 | 120 | if (1): 121 | traverse_dxf("/bubba/electronicsDS/fusion/mountingholes.dxf", 122 | mounting_actions(board, 123 | {"2.5": (testpad_lib, "TestPoint_Pad_D2.5mm")}, 124 | flip=True)) 125 | traverse_dxf("/bubba/electronicsDS/fusion/mountingholes.dxf", 126 | mounting_actions(board, 127 | {"2.5": (testpad_lib, "TestPoint_Pad_D2.5mm")})) 128 | 129 | 130 | if (0): 131 | traverse_graphics(board, "B.SilkS", 132 | segment_actions(board, layertable['Cmts.User']), 133 | merge_polys=True, 134 | break_curves=True) 135 | 136 | 137 | if (0): 138 | traverse_dxf("/bubba/electronicsDS/fusion/powerrails.dxf", 139 | segment_actions(board, layertable['B.SilkS']), 140 | merge_polys=False, 141 | break_curves=True 142 | ) 143 | 144 | if (0): 145 | traverse_dxf("/bubba/electronicsDS/fusion/solder_mask.dxf", 146 | segment_actions(board, layertable['F.Mask']), 147 | merge_polys=True, 148 | break_curves=True 149 | ) 150 | traverse_dxf("/bubba/electronicsDS/fusion/solder_mask.dxf", 151 | segment_actions(board, layertable['B.Mask']), 152 | merge_polys=True, 153 | break_curves=True 154 | ) 155 | 156 | 157 | pcbnew.Refresh() 158 | 159 | -------------------------------------------------------------------------------- /gen_border/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | print("adding genborderplugin") 3 | 4 | import pcbnew 5 | from . import gen_border 6 | 7 | class GenBorderPlugin(pcbnew.ActionPlugin): 8 | def defaults(self): 9 | self.name = "Generate Border" 10 | self.category = "A descriptive category name" 11 | self.description = "This plugin shrinks a rectangular boarder around the stuff in your design" 12 | 13 | def Run(self): 14 | # The entry function of the plugin that is executed on user action 15 | gen_border.GenerateBoarder() 16 | 17 | 18 | GenBorderPlugin().register() # Instantiate and register to Pcbnew 19 | 20 | print("done adding genborderplugin") 21 | -------------------------------------------------------------------------------- /gen_border/gen_border.py: -------------------------------------------------------------------------------- 1 | # Copyright [2017] [Miles McCoo] 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # this script is a basic svg generator for pcbnew. 16 | # the point of the script is more as a teaching tool 17 | # there are a number of ways in which is is deficient. 18 | 19 | 20 | import pcbnew 21 | 22 | def mymin(a,b): 23 | if (a == None): 24 | return b 25 | if (b == None): 26 | return a 27 | if (ab): 37 | return a 38 | return b 39 | 40 | 41 | class BBox: 42 | def __init__(self, xl=None, yl=None, xh=None, yh=None): 43 | self.xl = xl 44 | self.xh = xh 45 | self.yl = yl 46 | self.yh = yh 47 | 48 | def __str__(self): 49 | return "({},{} {},{})".format(self.xl, self.yl, self.xh, self.yh) 50 | 51 | def addPoint(self, pt): 52 | self.xl = mymin(self.xl, pt.x) 53 | self.xh = mymax(self.xh, pt.x) 54 | self.yl = mymin(self.yl, pt.y) 55 | self.yh = mymax(self.yh, pt.y) 56 | 57 | def addPointBloatXY(self, pt, x, y): 58 | self.xl = mymin(self.xl, pt.x-x) 59 | self.xh = mymax(self.xh, pt.x+x) 60 | self.yl = mymin(self.yl, pt.y-y) 61 | self.yh = mymax(self.yh, pt.y+y) 62 | 63 | 64 | 65 | def GenerateBoarder(): 66 | print("running generate boarder") 67 | 68 | board = pcbnew.GetBoard() 69 | 70 | padshapes = { 71 | pcbnew.PAD_SHAPE_CIRCLE: "PAD_SHAPE_CIRCLE", 72 | pcbnew.PAD_SHAPE_OVAL: "PAD_SHAPE_OVAL", 73 | pcbnew.PAD_SHAPE_RECT: "PAD_SHAPE_RECT", 74 | pcbnew.PAD_SHAPE_TRAPEZOID: "PAD_SHAPE_TRAPEZOID" 75 | } 76 | # new in the most recent kicad code 77 | if hasattr(pcbnew, 'PAD_SHAPE_ROUNDRECT'): 78 | padshapes[pcbnew.PAD_SHAPE_ROUNDRECT] = "PAD_SHAPE_ROUNDRECT", 79 | 80 | # generate a name->layer table so we can lookup layer numbers by name. 81 | layertable = {} 82 | 83 | # if you get an error saying that PCB_LAYER_ID_COUNT doesn't exist, then 84 | # it's probably because you're on an older version of pcbnew. 85 | # the interface has changed a little (progress) it used to be called LAYER_ID_COUNT. 86 | # now it's called PCB_LAYER_ID_COUNT 87 | if hasattr(pcbnew, "LAYER_ID_COUNT"): 88 | pcbnew.PCB_LAYER_ID_COUNT = pcbnew.LAYER_ID_COUNT 89 | 90 | 91 | numlayers = pcbnew.PCB_LAYER_ID_COUNT 92 | for i in range(numlayers): 93 | layertable[board.GetLayerName(i)] = i 94 | #print("{} {}".format(i, board.GetLayerName(i))) 95 | 96 | 97 | # the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm) 98 | # the coordinate 121550000 corresponds to 121.550000 99 | SCALE = 1000000.0 100 | 101 | boardbbox = BBox(); 102 | 103 | print("getting tracks") 104 | alltracks = board.GetTracks() 105 | for track in alltracks: 106 | # print("{}->{}".format(track.GetStart(), track.GetEnd())) 107 | # print("{},{}->{},{} width {} layer {}".format(track.GetStart().x/SCALE, track.GetStart().y/SCALE, 108 | # track.GetEnd().x/SCALE, track.GetEnd().y/SCALE, 109 | # track.GetWidth()/SCALE, 110 | # track.GetLayer()) 111 | # ) 112 | boardbbox.addPoint(track.GetStart()) 113 | boardbbox.addPoint(track.GetEnd()) 114 | 115 | print("getting pads") 116 | allpads = board.GetPads() 117 | for pad in allpads: 118 | #print("pad {} {}".format(pad.GetParent().GetReference(), pad.GetPadName())) 119 | if (pad.GetShape() == pcbnew.PAD_SHAPE_RECT): 120 | if ((pad.GetOrientationDegrees()==270) | (pad.GetOrientationDegrees()==90)): 121 | boardbbox.addPointBloatXY(pad.GetPosition(), pad.GetSize().y/2, pad.GetSize().x/2) 122 | else: 123 | boardbbox.addPointBloatXY(pad.GetPosition(), pad.GetSize().x/2, pad.GetSize().y/2) 124 | elif (pad.GetShape() == pcbnew.PAD_SHAPE_CIRCLE): 125 | boardbbox.addPointBloatXY(pad.GetPosition(), pad.GetSize().x/2, pad.GetSize().y/2) 126 | 127 | elif (pad.GetShape() == pcbnew.PAD_SHAPE_OVAL): 128 | boardbbox.addPointBloatXY(pad.GetPosition(), pad.GetSize().x/2, pad.GetSize().y/2) 129 | else: 130 | print("unknown pad shape {}({})".format(pad.GetShape(), padshapes[pad.GetShape()])) 131 | 132 | 133 | for mod in board.GetModules(): 134 | #print("for mod {}".format(mod.GetReference())) 135 | #print("bbox is {}".format(str(boardbbox))) 136 | for gi in mod.GraphicalItems(): 137 | bbox = gi.GetBoundingBox() 138 | boardbbox.addPointBloatXY(bbox.Centre(), bbox.GetWidth()/2, bbox.GetHeight()/2) 139 | 140 | 141 | 142 | 143 | for d in board.GetDrawings(): 144 | #print("{}".format(str(d))) 145 | #print("on layer {} {} {}".format(d.GetLayerName(), 146 | # str(d.GetStart()), 147 | # str(d.GetEnd()))) 148 | if (d.GetLayerName() == 'Edge.Cuts'): 149 | board.Remove(d) 150 | 151 | 152 | edgecut = layertable['Edge.Cuts'] 153 | 154 | seg1 = pcbnew.DRAWSEGMENT(board) 155 | board.Add(seg1) 156 | seg1.SetStart(pcbnew.wxPoint(boardbbox.xl, boardbbox.yl)) 157 | seg1.SetEnd( pcbnew.wxPoint(boardbbox.xl, boardbbox.yh)) 158 | seg1.SetLayer(edgecut) 159 | 160 | seg1 = pcbnew.DRAWSEGMENT(board) 161 | board.Add(seg1) 162 | seg1.SetStart(pcbnew.wxPoint(boardbbox.xl, boardbbox.yh)) 163 | seg1.SetEnd( pcbnew.wxPoint(boardbbox.xh, boardbbox.yh)) 164 | seg1.SetLayer(edgecut) 165 | 166 | seg1 = pcbnew.DRAWSEGMENT(board) 167 | board.Add(seg1) 168 | seg1.SetStart(pcbnew.wxPoint(boardbbox.xh, boardbbox.yh)) 169 | seg1.SetEnd( pcbnew.wxPoint(boardbbox.xh, boardbbox.yl)) 170 | seg1.SetLayer(edgecut) 171 | 172 | seg1 = pcbnew.DRAWSEGMENT(board) 173 | board.Add(seg1) 174 | seg1.SetStart(pcbnew.wxPoint(boardbbox.xh, boardbbox.yl)) 175 | seg1.SetEnd( pcbnew.wxPoint(boardbbox.xl, boardbbox.yl)) 176 | seg1.SetLayer(edgecut) 177 | 178 | 179 | # from PolyLine.h 180 | # // 181 | # // A polyline contains one or more contours, where each contour 182 | # // is defined by a list of corners and side-styles 183 | # // There may be multiple contours in a polyline. 184 | # // The last contour may be open or closed, any others must be closed. 185 | # // All of the corners and side-styles are concatenated into 2 arrays, 186 | # // separated by setting the end_contour flag of the last corner of 187 | # // each contour. 188 | # // 189 | # // When used for copper (or technical layers) areas, the first contour is the outer edge 190 | # // of the area, subsequent ones are "holes" in the copper. 191 | 192 | nets = board.GetNetsByName() 193 | 194 | powernets = [] 195 | for name in ["+12V", "+5V"]: 196 | if (nets.has_key(name)): 197 | powernets.append((name, "B.Cu")) 198 | break 199 | 200 | for name in ["GND"]: 201 | if (nets.has_key(name)): 202 | powernets.append((name, "F.Cu")) 203 | break 204 | 205 | 206 | for netname,layername in (powernets): 207 | net = nets.find(netname).value()[1] 208 | layer = layertable[layername] 209 | newarea = board.InsertArea(net.GetNet(), 0, layer, boardbbox.xl, boardbbox.yl, pcbnew.CPolyLine.DIAGONAL_EDGE) 210 | 211 | newoutline = newarea.Outline() 212 | 213 | # if you get a crash here, it's because you're on an older version of pcbnew. 214 | # the data structs for polygons has changed a little. The old struct has a 215 | # method called AppendCorner. Now it's just Append. Also, the call to CloseLastContour, 216 | # commented below used to be needed to avoid a corrupt output file. 217 | newoutline.Append(boardbbox.xl, boardbbox.yh); 218 | newoutline.Append(boardbbox.xh, boardbbox.yh); 219 | newoutline.Append(boardbbox.xh, boardbbox.yl); 220 | # this next line shouldn't really be necessary but without it, saving to 221 | # file will yield a file that won't load. 222 | # newoutline.CloseLastContour() 223 | 224 | # don't know why this is necessary. When calling InsertArea above, DIAGONAL_EDGE was passed 225 | # If you save/restore, the zone will come back hatched. 226 | # before then, the zone boundary will just be a line. 227 | # Omit this if you are using pcbnew.CPolyLine.NO_HATCH 228 | newarea.Hatch() 229 | 230 | 231 | # In the future, this build connectivity call will not be neccessary. 232 | # I have submitted a patch to include this in the code for Refresh. 233 | # You'll know you needed it if pcbnew crashes without it. 234 | # later note: when running as a plugin, the plugin mechanism deals with 235 | # this kind of stuff for you. 236 | #board.BuildConnectivity() 237 | 238 | #pcbnew.Refresh() 239 | print("done") 240 | -------------------------------------------------------------------------------- /gensvg/gensvg.py: -------------------------------------------------------------------------------- 1 | # Copyright [2017] [Miles McCoo] 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # this script is a basic svg generator for pcbnew. 16 | # the point of the script is more as a teaching tool 17 | # there are a number of ways in which is is deficient. 18 | 19 | 20 | import pcbnew 21 | 22 | # if you get the error: importError: No module named svgwrite 23 | # you need to do "pip install svgwrite" in an xterm 24 | 25 | 26 | 27 | padshapes = { 28 | pcbnew.PAD_SHAPE_CIRCLE: "PAD_SHAPE_CIRCLE", 29 | pcbnew.PAD_SHAPE_OVAL: "PAD_SHAPE_OVAL", 30 | pcbnew.PAD_SHAPE_RECT: "PAD_SHAPE_RECT", 31 | pcbnew.PAD_SHAPE_TRAPEZOID: "PAD_SHAPE_TRAPEZOID" 32 | } 33 | # new in the most recent kicad code 34 | if hasattr(pcbnew, 'PAD_SHAPE_ROUNDRECT'): 35 | padshapes[pcbnew.PAD_SHAPE_ROUNDRECT] = "PAD_SHAPE_ROUNDRECT", 36 | 37 | 38 | 39 | board = pcbnew.GetBoard() 40 | 41 | boardbbox = board.ComputeBoundingBox() 42 | boardxl = boardbbox.GetX() 43 | boardyl = boardbbox.GetY() 44 | boardwidth = boardbbox.GetWidth() 45 | boardheight = boardbbox.GetHeight() 46 | 47 | # coordinate space of kicad_pcb is in mm. At the beginning of 48 | # https://en.wikibooks.org/wiki/Kicad/file_formats#Board_File_Format 49 | # "All physical units are in mils (1/1000th inch) unless otherwise noted." 50 | # then later in historical notes, it says, 51 | # As of 2013, the PCBnew application creates ".kicad_pcb" files that begin with 52 | # "(kicad_pcb (version 3)". All distances are in millimeters. 53 | 54 | # the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm) 55 | # the coordinate 121550000 corresponds to 121.550000 56 | 57 | SCALE = 1000000.0 58 | 59 | 60 | import sys, os 61 | import svgwrite 62 | 63 | print("working in the dir " + os.getcwd()) 64 | name = "output.svg" 65 | # A4 is approximately 21x29 66 | dwg = svgwrite.Drawing(name, size=('21cm', '29cm'), profile='full', debug=True) 67 | 68 | dwg.viewbox(width=boardwidth, height=boardheight, minx=boardxl, miny=boardyl) 69 | background = dwg.add(dwg.g(id='bg', stroke='white')) 70 | background.add(dwg.rect(insert=(boardxl, boardyl), size=(boardwidth, boardheight), fill='white')) 71 | 72 | 73 | 74 | svglayers = {} 75 | colors = board.Colors() 76 | for layerid in range(pcbnew.PCB_LAYER_ID_COUNT): 77 | c4 = colors.GetLayerColor(layerid); 78 | colorrgb = "rgb({:d}, {:d}, {:d})".format(int(round(c4.r*255)), 79 | int(round(c4.g*255)), 80 | int(round(c4.b*255))); 81 | layer = dwg.add(dwg.g(id='layer_'+str(layerid), stroke=colorrgb, stroke_linecap="round")) 82 | svglayers[layerid] = layer 83 | 84 | alltracks = board.GetTracks() 85 | for track in alltracks: 86 | # print("{}->{}".format(track.GetStart(), track.GetEnd())) 87 | # print("{},{}->{},{} width {} layer {}".format(track.GetStart().x/SCALE, track.GetStart().y/SCALE, 88 | # track.GetEnd().x/SCALE, track.GetEnd().y/SCALE, 89 | # track.GetWidth()/SCALE, 90 | # track.GetLayer()) 91 | # ) 92 | svglayers[track.GetLayer()].add(dwg.line(start=(track.GetStart().x, 93 | track.GetStart().y), 94 | end=(track.GetEnd().x, 95 | track.GetEnd().y), 96 | stroke_width=track.GetWidth() 97 | )) 98 | 99 | 100 | svgpads = dwg.add(dwg.g(id='pads', stroke='red',fill='orange')) 101 | allpads = board.GetPads() 102 | 103 | for pad in allpads: 104 | mod = pad.GetParent() 105 | name = pad.GetPadName() 106 | if (0): 107 | print("pad {}({}) on {}({}) at {},{} shape {} size {},{}" 108 | .format(name, 109 | pad.GetNet().GetNetname(), 110 | mod.GetReference(), 111 | mod.GetValue(), 112 | pad.GetPosition().x, pad.GetPosition().y, 113 | padshapes[pad.GetShape()], 114 | pad.GetSize().x, pad.GetSize().y 115 | )) 116 | if (pad.GetShape() == pcbnew.PAD_SHAPE_RECT): 117 | if ((pad.GetOrientationDegrees()==270) | (pad.GetOrientationDegrees()==90)): 118 | svgpads.add(dwg.rect(insert=(pad.GetPosition().x-pad.GetSize().x/2, 119 | pad.GetPosition().y-pad.GetSize().y/2), 120 | size=(pad.GetSize().y, pad.GetSize().x))) 121 | else: 122 | svgpads.add(dwg.rect(insert=(pad.GetPosition().x-pad.GetSize().x/2, 123 | pad.GetPosition().y-pad.GetSize().y/2), 124 | size=(pad.GetSize().x, pad.GetSize().y))) 125 | elif (pad.GetShape() == pcbnew.PAD_SHAPE_CIRCLE): 126 | svgpads.add(dwg.circle(center=(pad.GetPosition().x, pad.GetPosition().y), 127 | r=pad.GetSize().x)) 128 | elif (pad.GetShape() == pcbnew.PAD_SHAPE_OVAL): 129 | svgpads.add(dwg.ellipse(center=(pad.GetPosition().x, pad.GetPosition().y), 130 | r=(pad.GetSize().x/2, pad.GetSize().y/2))) 131 | else: 132 | print("unknown pad shape {}({})".format(pad.GetShape(), padshapes[pad.GetShape()])) 133 | 134 | 135 | 136 | dwg.save() 137 | 138 | 139 | print("done") 140 | -------------------------------------------------------------------------------- /instantiate_footprint/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import pcbnew 3 | 4 | from . import instantiate_footprint 5 | 6 | 7 | class AddMountingHolesPlugin(pcbnew.ActionPlugin): 8 | def defaults(self): 9 | self.name = "Add Mounting Holes" 10 | self.category = "A descriptive category name" 11 | self.description = "This plugin adds mounting holes above the top and bottom of your design" 12 | 13 | def Run(self): 14 | # The entry function of the plugin that is executed on user action 15 | instantiate_footprint.AddMountingHoles() 16 | 17 | 18 | AddMountingHolesPlugin().register() # Instantiate and register to Pcbnew 19 | -------------------------------------------------------------------------------- /instantiate_footprint/instantiate_footprint.py: -------------------------------------------------------------------------------- 1 | import pcbnew 2 | 3 | def GetRectCorners(rect): 4 | return [pcbnew.wxPoint(rect.Centre().x-rect.GetWidth()/2, rect.Centre().y-rect.GetHeight()/2), 5 | pcbnew.wxPoint(rect.Centre().x-rect.GetWidth()/2, rect.Centre().y+rect.GetHeight()/2), 6 | pcbnew.wxPoint(rect.Centre().x+rect.GetWidth()/2, rect.Centre().y+rect.GetHeight()/2), 7 | pcbnew.wxPoint(rect.Centre().x+rect.GetWidth()/2, rect.Centre().y-rect.GetHeight()/2)] 8 | 9 | # GetBoundingBox includes the text stuff. 10 | def GetModBBox(mod): 11 | modbox = None 12 | for pad in mod.Pads(): 13 | #print("pad on layer {}".format(pad.GetLayerName())) 14 | if (modbox == None): 15 | modbox = pad.GetBoundingBox() 16 | else: 17 | modbox.Merge(pad.GetBoundingBox()) 18 | for gi in mod.GraphicalItems(): 19 | #print("pad gi on layer {}".format(gi.GetLayerName())); 20 | if (modbox == None): 21 | modbox = gi.GetBoundingBox() 22 | else: 23 | modbox.Merge(gi.GetBoundingBox()) 24 | 25 | return modbox 26 | 27 | def AddMountingHoles(): 28 | footprint_lib = '/home/mmccoo/kicad/kicad-footprints/MountingHole.pretty' 29 | 30 | board = pcbnew.GetBoard() 31 | 32 | # the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm) 33 | # the coordinate 121550000 corresponds to 121.550000 34 | 35 | SCALE = 1000000.0 36 | 37 | rect = None 38 | for d in board.GetDrawings(): 39 | if (d.GetLayerName() != "Edge.Cuts"): 40 | continue 41 | if (rect == None): 42 | rect = d.GetBoundingBox() 43 | else: 44 | rect.Merge(d.GetBoundingBox()) 45 | #print("{}".format(str(d))) 46 | #print("on layer {} {} {}".format(d.GetLayerName(), 47 | # str(d.GetStart()), 48 | # str(d.GetEnd()))) 49 | 50 | 51 | print("bbox of boundary is centered at {}. Width: {}, Height {}".format(rect.Centre(), 52 | rect.GetWidth(), 53 | rect.GetHeight())) 54 | print("left {} {} {} {}".format(rect.GetLeft(), 55 | rect.GetBottom(), 56 | rect.GetRight(), 57 | rect.GetTop())) 58 | 59 | 60 | 61 | 62 | 63 | io = pcbnew.PCB_IO() 64 | board = pcbnew.GetBoard() 65 | 66 | mod = io.FootprintLoad(footprint_lib, "MountingHole_3.2mm_M3") 67 | 68 | # what I really want to do is inflating by a negative amount, 69 | # but that function takes a xwCoord, which I don't know how 70 | # to create given the current python interface. 71 | # in this case we want to compute where the mounting 72 | # holes should go. 73 | # I am reducing with the full width/height of the module because 74 | # adjusting width/height of the rect needs both sides 75 | modbox = GetModBBox(mod); 76 | rect.SetWidth(rect.GetWidth() - modbox.GetWidth()) 77 | rect.SetHeight(rect.GetHeight() + modbox.GetHeight()) 78 | rect.SetX(rect.GetX() + modbox.GetWidth()/2) 79 | rect.SetY(rect.GetY() - modbox.GetHeight()/2) 80 | print("new bbox of boundary is centered at {}. Width: {}, Height {}".format(rect.Centre(), 81 | rect.GetWidth(), 82 | rect.GetHeight())) 83 | print("left {} {} {} {}".format(rect.GetLeft(), 84 | rect.GetBottom(), 85 | rect.GetRight(), 86 | rect.GetTop())) 87 | 88 | # this is here for testing on an empty design 89 | # for mod in board.GetModules(): 90 | # board.Remove(mod) 91 | 92 | for point in GetRectCorners(rect): 93 | # this looks like a redundant call given the similar call above. 94 | # this call basically instantiates a new one. We don't want to add it twice. 95 | mod = io.FootprintLoad(footprint_lib, "MountingHole_3.2mm_M3") 96 | print("location {}".format(point)) 97 | modbox = GetModBBox(mod) 98 | 99 | point.x = point.x - modbox.Centre().x + mod.GetPosition().x 100 | point.y = point.y - modbox.Centre().y + mod.GetPosition().y 101 | mod.SetPosition(point) 102 | print("mod pos {}, {}".format(point, modbox.Centre())) 103 | print("x {} y {}".format(point.x - modbox.Centre().x, 104 | point.y - modbox.Centre().y)) 105 | 106 | board.Add(mod) 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | # In the future, this build connectivity call will not be neccessary. 119 | # I have submitted a patch to include this in the code for Refresh. 120 | # You'll know you needed it if pcbnew crashes without it. 121 | board.BuildConnectivity() 122 | 123 | pcbnew.Refresh() 124 | print("done") 125 | -------------------------------------------------------------------------------- /menus_and_buttons/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmccoo/kicad_mmccoo/930a9f1114a170bee9cd4ef013bb8f935864296b/menus_and_buttons/hello.png -------------------------------------------------------------------------------- /menus_and_buttons/menus_and_buttons.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import pcbnew 4 | import wx 5 | import wx.aui 6 | 7 | 8 | # get the path of this script. Will need it to load the png later. 9 | import inspect 10 | import os 11 | filename = inspect.getframeinfo(inspect.currentframe()).filename 12 | path = os.path.dirname(os.path.abspath(filename)) 13 | print("running {} from {}".format(filename, path)) 14 | 15 | 16 | def findPcbnewWindow(): 17 | windows = wx.GetTopLevelWindows() 18 | pcbnew = [w for w in windows if w.GetTitle()[0:6] == "Pcbnew"] 19 | if len(pcbnew) != 1: 20 | raise Exception("Cannot find pcbnew window from title matching!") 21 | return pcbnew[0] 22 | 23 | pcbwin = findPcbnewWindow() 24 | 25 | 26 | # 6038 is the value that H_TOOLBAR from kicad/include/id.h happens to get. 27 | # other interesting values include: 28 | # 6041 is AUX_TOOLBAR. That's the second row of stuff in the pcbnew gui. 29 | # it contains things like track width, via size, grid 30 | # 6039 is V_TOOLBAR, the right commands window. zoom to selection, highlight net. 31 | # 6040 is OPT_TOOLBAR, the left commands window. disable drc, hide grid, display polar 32 | 33 | # kicad/include/id.h has been added to pcbnew's interface. If you get the error 34 | # that ID_H_TOOLBAR doesn't exist, it's probably because you need to update your 35 | # version of kicad. 36 | top_tb = pcbwin.FindWindowById(pcbnew.ID_H_TOOLBAR) 37 | 38 | 39 | 40 | # let's look at what top level frames/windows we have. These include the 41 | # 42 | #children = {} 43 | #for subwin in pcbwin.Children: 44 | # id = subwin.GetId() 45 | # children[id] = subwin 46 | # print("subwin {} {} {}".format(subwin.GetLabel(), subwin.GetClassName(), subwin.GetId())) 47 | 48 | # for idx in range(top_tb.GetToolCount()): 49 | # tbi = top_tb.FindToolByIndex(idx) 50 | # #print("toolbar item {}".format(tbi.GetShortHelp())) 51 | 52 | 53 | 54 | def MyButtonsCallback(event): 55 | # when called as a callback, the output of this print 56 | # will appear in your xterm or wherever you invoked pcbnew. 57 | print("got a click on my new button {}".format(str(event))) 58 | 59 | 60 | 61 | 62 | 63 | # Plan for three sizes of bitmaps: 64 | # SMALL - for menus - 16 x 16 65 | # MID - for toolbars - 26 x 26 66 | # BIG - for program icons - 48 x 48 67 | # bitmaps_png/CMakeLists.txt 68 | 69 | bm = wx.Bitmap(path + '/hello.png', wx.BITMAP_TYPE_PNG) 70 | 71 | 72 | itemid = wx.NewId() 73 | top_tb.AddTool(itemid, "mybutton", bm, "this is my button", wx.ITEM_NORMAL) 74 | top_tb.Bind(wx.EVT_TOOL, MyButtonsCallback, id=itemid) 75 | top_tb.Realize() 76 | 77 | -------------------------------------------------------------------------------- /place_by_sch/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import pcbnew 3 | 4 | from . import place_by_sch 5 | 6 | 7 | class PlaceBySchPlugin(pcbnew.ActionPlugin): 8 | def defaults(self): 9 | self.name = "Place By Sch" 10 | self.category = "A descriptive category name" 11 | self.description = "This plugin reads the .sch file and apply its placements to the current design" 12 | 13 | def Run(self): 14 | # The entry function of the plugin that is executed on user action 15 | place_by_sch.PlaceBySch() 16 | 17 | 18 | PlaceBySchPlugin().register() # Instantiate and register to Pcbnew 19 | -------------------------------------------------------------------------------- /place_by_sch/place_by_sch.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import pcbnew 4 | import os.path 5 | import re 6 | 7 | 8 | def PlaceBySch(): 9 | board = pcbnew.GetBoard() 10 | 11 | board_path = board.GetFileName() 12 | sch_path = board_path.replace(".kicad_pcb", ".sch") 13 | 14 | if (not os.path.isfile(sch_path)): 15 | raise ValueError("file {} doesn't exist".format(sch_path)) 16 | 17 | # some documentation on eeschema file format 18 | # https://en.wikibooks.org/wiki/Kicad/file_formats#Schematic_Files_Format 19 | # schematic files are written here: 20 | # eeschema/sch_legacy_plugin.cpp 21 | # rotation is given here: 22 | # TRANSFORM transform = aComponent->GetTransform(); 23 | # m_out->Print( 0, "\t%-4d %-4d %-4d %-4d\n", 24 | # transform.x1, transform.y1, transform.x2, transform.y2 ); 25 | 26 | # $Comp 27 | # L Device:LED D49 28 | # U 1 1 5A3B7115 29 | # P 6650 1500 30 | # F 0 "D49" H 6641 1716 50 0000 C CNN 31 | # F 1 "LED" H 6641 1625 50 0000 C CNN 32 | # F 2 "Miles:LED_5730" H 6650 1500 50 0001 C CNN 33 | # F 3 "~" H 6650 1500 50 0001 C CNN 34 | # 1 6650 1500 35 | # 1 0 0 -1 36 | # $EndComp 37 | 38 | newcomp_p = re.compile('\$Comp') 39 | endcomp_p = re.compile('\$EndComp') 40 | comp_label_p = re.compile('L (\S+) (\S+)') 41 | trans_p = re.compile('\t([\-0-9]+)\s+([\-0-9]+)\s+([\-0-9]+)\s+([\-0-9]+)') 42 | footprint_p = re.compile('F 2 "(\S+)" [VH] (\d+) (\d+)') 43 | 44 | c1 = '$Comp' 45 | l1 = 'L Device:LED D49' 46 | t0 = ' 1 0 0 -1' 47 | t0f = ' 1 0 0 1' 48 | t180 = ' -1 0 0 1' 49 | t180f = ' -1 0 0 -1' # this is flipped horizontally 50 | tM90 = ' 0 1 1 0' 51 | t90 = ' 0 -1 -1 0' 52 | orientations = { 53 | str(trans_p.match(t0).groups()): 0, 54 | str(trans_p.match(t0f).groups()): 0, # TWS added to handle flipped, but no rotation 55 | str(trans_p.match(t180).groups()): 180, 56 | str(trans_p.match(t180f).groups()): 180, 57 | str(trans_p.match(tM90).groups()): -90, 58 | str(trans_p.match(t90).groups()): 90 59 | } 60 | 61 | def parse_eeschema(filename): 62 | retval = {} 63 | with open(filename) as f: 64 | incomp = False 65 | curcomp = ""; 66 | x = -1 67 | y = -1 68 | orient = 0; 69 | for line in f: 70 | bc = newcomp_p.match(line) 71 | ec = endcomp_p.match(line) 72 | l = comp_label_p.match(line) 73 | t = trans_p.match(line) 74 | fp = footprint_p.match(line) 75 | if (bc): 76 | incomp = True 77 | #print("new comp") 78 | elif (ec): 79 | incomp = False 80 | retval[curcomp] = [x, y, orient] 81 | x = -1 82 | y = -1 83 | curcomp = "" 84 | orient = 0; 85 | #print("end comp") 86 | 87 | if (not incomp): 88 | continue 89 | 90 | if (l): 91 | curcomp = l.groups()[1]; 92 | #print("l {} {}".format(l.groups()[0], l.groups()[1])) 93 | elif (t): 94 | orient = orientations[str(t.groups())] 95 | #print("orient {}".format(orient)) 96 | elif (fp): 97 | x = int(fp.groups()[1]) 98 | y = int(fp.groups()[2]) 99 | #print("location {} {}".format(x,y)) 100 | 101 | return retval; 102 | 103 | 104 | locs = parse_eeschema(sch_path) 105 | 106 | for mod in board.GetModules(): 107 | ref = mod.GetReference() 108 | 109 | if (ref not in locs): 110 | print("couldn't get loc info for {}".format(ref)) 111 | continue 112 | 113 | # eeschema stores stuff in 1000ths of an inch. 114 | # pcbnew stores in 10e-6mm 115 | # 1000ths inch * inch/1000ths inch * 25.4mm/inch * 10e6 116 | # oldvalue * 25.4 / 10e4 117 | newx = locs[ref][0] * 25.4 * 1000.0 118 | newy = locs[ref][1] * 25.4 * 1000.0 119 | mod.SetPosition(pcbnew.wxPoint(int(newx), int(newy))) 120 | mod.SetOrientation(locs[ref][2]*10) 121 | print("placing {} at {},{}".format(ref, newx, newy)) 122 | 123 | # when running as a plugin, this isn't needed. it's done for you 124 | #pcbnew.Refresh(); 125 | -------------------------------------------------------------------------------- /plantuml/README: -------------------------------------------------------------------------------- 1 | This is a spec for pcbnew's class hierarchy. 2 | 3 | This file is processed using (the very nice) plantuml tool: 4 | http://plantuml.com/ 5 | 6 | This spec doesn't include everything available pcbnew, just the classes and APIs that I've found necessary to do useful work. 7 | 8 | -------------------------------------------------------------------------------- /plantuml/pcbnew_uml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmccoo/kicad_mmccoo/930a9f1114a170bee9cd4ef013bb8f935864296b/plantuml/pcbnew_uml.png -------------------------------------------------------------------------------- /plantuml/pcbnew_uml.puml: -------------------------------------------------------------------------------- 1 | # Copyright [2017] [Miles McCoo] 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | @startuml 17 | 18 | pcbnew --> Board 19 | Board --> NetMap 20 | Board o-- "0..*" Pad 21 | Board o-- "0..*" Module 22 | Board o-- ZoneContainer 23 | 24 | NetMap o-- "0..*" Net 25 | 26 | Pad --> wxPoint 27 | Pad --> Module 28 | Pad --> wxSize 29 | Pad --> PadShape 30 | Board --> EDA_RECT 31 | Board o-- Track 32 | Board o-- DrawSegment 33 | Module o-- Pad 34 | Module o-- DrawSegment 35 | 36 | ZoneContainer --> CPolyLine 37 | 38 | Net --> NetClass 39 | Net o-- Pad 40 | Net --> Board 41 | Track --> Net 42 | Track --> wxPoint 43 | 44 | Track <|-- Via 45 | Via --> ViaType 46 | 47 | class pcbnew { 48 | Board GetBoard() 49 | Track TRACK(board) 50 | Via VIA(board) 51 | 52 | int LAYER_ID_COUNT 53 | } 54 | 55 | class Board { 56 | NetMap GetNetsByNetcode() 57 | NetMap GetNetsByName() 58 | Pad[] GetPads() 59 | EDA_RECT ComputeBoundingBox() 60 | Track[] TracksInNet(int netcode) 61 | # GetTracks() actually returns a track_list 62 | # tracks inclue Vias 63 | Track[] GetTracks() 64 | # there's not an exposed way to cast track->via 65 | VIA GetViaByPosition(wxPoint) 66 | Module[] GetModules() 67 | Module FindModuleByReference(string) 68 | string GetLayerName(int) 69 | # zone stuff 70 | int GetAreaCount() 71 | ZoneContainer GetArea(int) 72 | -- 73 | Add(Track) 74 | Remove(Track) 75 | Add(DrawSegment) 76 | Remove(DrawSegment) 77 | 78 | ZoneContainer InsertArea(int netcode, 0, int layer, int x, int y, int hatch) 79 | } 80 | 81 | class DrawSegment { 82 | string GetLayerName() 83 | wxPoint GetStart() 84 | wxPoint GetEnd() 85 | EDA_RECT GetBoundingBox() 86 | -- 87 | SetStart(wxPoint) 88 | SetEnd(wxPoint) 89 | SetLayer(int) 90 | } 91 | 92 | class ZoneContainer { 93 | int GetLayer() 94 | string GetLayerName() 95 | Net GetNet() 96 | 97 | CPolyLine Outline() 98 | } 99 | 100 | class CPolyLine { 101 | int GetContoursCount() 102 | int GetContourStart(int) 103 | int GetContourEnd(int) 104 | int GetContourSize(int) 105 | int GetX(int) 106 | int GetY(int) 107 | int GetPos(int) 108 | 109 | int NO_HATCH 110 | int DIAGONAL_FULL 111 | int DIAGONAL_EDGE 112 | 113 | -- 114 | AppendCorner(int x, int y) 115 | } 116 | 117 | class Pad { 118 | string GetPadName() 119 | Net GetNet() 120 | wxPoint GetPosition() 121 | Module GetParent() 122 | wxSize GetSize() # this is affected by the orientation 123 | double GetOrientationDegrees() 124 | int GetShape() # compare to PadShapevalues 125 | } 126 | 127 | class Module { 128 | -- 129 | string GetReference() 130 | string GetValue() 131 | Pad[] Pads() 132 | # elements of subsheets 133 | # will have similar paths 134 | string GetPath() 135 | # GraphicalItems actually returns EDGE_MODULES but those 136 | # derive from DrawSegment 137 | DrawSegment[] GraphicalItems() 138 | -- 139 | SetPosition(pcbnew.wxPoint(x,y)) 140 | SetOrientation(degrees*10.0) 141 | } 142 | 143 | class NetMap { 144 | Net[] items() 145 | NetIterator find() 146 | } 147 | 148 | class Net { 149 | NetClass GetNetClass() 150 | int GetNet() # netcode 151 | string GetNetname() 152 | Pad[] Pads() 153 | Board GetParent() 154 | -- 155 | To get tracks, use Board::TracksInNet 156 | } 157 | 158 | NetClass : int GetTrackWidth() 159 | 160 | class wxPoint { 161 | int x 162 | int y 163 | } 164 | 165 | class wxSize { 166 | int x 167 | int y 168 | } 169 | 170 | class EDA_RECT { 171 | wxPoint Centre() 172 | int GetX() 173 | int GetY() 174 | int GetWidth() 175 | int GetHeight() 176 | } 177 | 178 | class Track { 179 | wxPoint GetStart() 180 | wxPoint GetEnd() 181 | int GetWidth() 182 | int GetLayer() 183 | Net GetNet() 184 | -- 185 | SetStart(pcbnew.wxPoint(int, int)) 186 | SetEnd(pcbnew.wxPoint(int, int)) 187 | SetWidth(int) 188 | SetLayer(int) 189 | 190 | 191 | SetNet(Net) 192 | } 193 | 194 | class Via { 195 | # I haven't found a way to get the layers directly. 196 | # unable to allocate the LAYER_ID objects 197 | LayerPair(LAYER_ID* top_layer, LAYER_ID* bottom_layer ) 198 | wxPoint GetPosition() 199 | ViaType GetViaType() 200 | int GetWidth() 201 | -- 202 | SetPosition(pcbnew.wxPoint(int, int)) 203 | SetViaType(ViaType) 204 | SetWidth(int) 205 | SetLayerPair(int top, int bottom) 206 | # iterate through pcbnew.LAYER_ID_COUNT 207 | bool IsOnLayer() 208 | } 209 | 210 | class PadShape { 211 | int pcbnew.PAD_SHAPE_CIRCLE 212 | int pcbnew.PAD_SHAPE_OVAL 213 | int pcbnew.PAD_SHAPE_RECT 214 | int pcbnew.PAD_SHAPE_ROUNDRECT 215 | int pcbnew.PAD_SHAPE_TRAPEZOID 216 | } 217 | 218 | class ViaType { 219 | int pcbnew.VIA_THROUGH 220 | int pcbnew.VIA_BLIND_BURIED 221 | int pcbnew.VIA_MICROVIA 222 | int pcbnew.VIA_NOT_DEFINED 223 | } 224 | @enduml 225 | 226 | -------------------------------------------------------------------------------- /python_usage_tracker/kicad_python_usage_tracker.py: -------------------------------------------------------------------------------- 1 | 2 | #import sys; sys.path.append("/home/mmccoo/kicad/kicad_mmccoo/python_usage_tracker"); import kicad_python_usage_tracker 3 | 4 | #import pcbnew; b = pcbnew.GetBoard(); b.Add() 5 | 6 | 7 | import pcbnew 8 | import types 9 | import inspect 10 | import pdb 11 | import numbers 12 | 13 | stats = {} 14 | method_stats = {} 15 | returned_vals = {} 16 | returned_ids = {} 17 | 18 | board = None 19 | 20 | file = open("test.py", "w") 21 | file.write("import pcbnew\n\n") 22 | 23 | def format_args(args): 24 | toprint = [] 25 | 26 | for arg in args: 27 | 28 | strval = str(arg) 29 | if (strval in returned_vals): 30 | toprint.append(returned_vals[strval]) 31 | continue 32 | 33 | if (isinstance(arg, numbers.Number)): 34 | toprint.append(str(arg)) 35 | continue 36 | 37 | # I believe that checking src and unicode is better than checking basestring because 38 | # basestring won't work in python 3.0 39 | if (isinstance(arg, pcbnew.wxString) or isinstance(arg, str) or isinstance(arg, unicode)): 40 | toprint.append("\"" + str(arg) + "\"") 41 | continue 42 | 43 | print("other type {}".format(arg)) 44 | toprint.append(str(arg)) 45 | 46 | inprint = False 47 | return (", ".join(toprint)) 48 | 49 | def format_retval(val): 50 | strval = str(val) 51 | 52 | if (isinstance(val, numbers.Number)): 53 | return (True, strval) 54 | 55 | if (isinstance(val, unicode)): 56 | return (True, "\"" + strval + "\"") 57 | 58 | if (isinstance(val, pcbnew.wxString)): 59 | return (True, "\"" + str(val) + "\"") 60 | 61 | if (hasattr(val, "__module__") and (val.__module__ != "pcbnew")): 62 | return (True, strval) 63 | 64 | if (strval in returned_vals): 65 | return (False, returned_vals[strval]) 66 | 67 | idx = returned_ids[val.__class__.__name__] = returned_ids.setdefault(val.__class__.__name__, -1) + 1 68 | retval = returned_vals[strval] = str(val.__class__.__name__) + "_" + str(idx) 69 | 70 | return (False, retval) 71 | 72 | 73 | def track_fn(func, name): 74 | def call(*args, **kwargs): 75 | 76 | global intrack 77 | intrack = intrack + 1 78 | 79 | result = func(*args, **kwargs) 80 | 81 | if (intrack == 1): 82 | stats[name] = stats.setdefault(name, 0) + 1 83 | 84 | argsstr = format_args(args) 85 | isconst, retstr = format_retval(result) 86 | 87 | callingframe = inspect.stack()[1] 88 | 89 | if (isconst): 90 | file.write("assert({} == {}({})) # {}, {}\n".format(retstr, name, argsstr, callingframe[1], callingframe[2])) 91 | else: 92 | file.write("{} = {}({}) # {}, {}\n".format(retstr, name, argsstr, callingframe[1], callingframe[2])) 93 | 94 | intrack = intrack - 1 95 | 96 | return result 97 | return call 98 | 99 | intrack = 0 100 | incrnum = 0 101 | mynum = 0 102 | def track_method(m, pathtohere, name): 103 | def call(*args, **kwargs): 104 | 105 | # printing calls str which may call other methods. 106 | # can't just use true/false, because printing (__str__) can trigger 107 | # more method calls. 108 | # also, need to be careful 109 | global intrack 110 | intrack = intrack + 1 111 | 112 | global incrnum 113 | incrnum = incrnum + 1 114 | 115 | global mynum 116 | 117 | # there are cases when python uses exceptions like out of range exception 118 | # as a way to know when to stop a loop. 119 | # getitem is one of the methods that I'm wrapping (like to get a net from a nettable) 120 | # for other methods, like wxPoint.__getitem__, I need to be sure to let the out of range 121 | # exception propagate. I also need to do some cleanup. 122 | try: 123 | result = m(*args, **kwargs) 124 | except Exception, e: 125 | intrack = intrack - 1 126 | raise 127 | #pdb.set_trace() 128 | #print("exception {}".format(str(e))) 129 | 130 | 131 | if (intrack == 1): 132 | callingframe = inspect.stack()[1] 133 | 134 | instclass = args[0].__class__ 135 | tbl = method_stats.setdefault(pathtohere + "." + name, {}) 136 | tbl[instclass] = tbl.setdefault(instclass, 0) + 1 137 | 138 | constself, slf = format_retval(args[0]) 139 | argsstr = format_args(args[1:]) 140 | 141 | isconst, retstr = format_retval(result) 142 | 143 | isconstructor = (name == "__init__") 144 | if (isconstructor): 145 | # constructor/__init__ doesn't return a value. The call to the constructor looks like 146 | # a method call, but it's syntactic sugar around the self that is passed around. 147 | file.write("{} = {}({}) # {}, {}\n".format(slf, pathtohere, argsstr, callingframe[1], callingframe[2])) 148 | elif (result is None): 149 | # it's important to do result is None instead of result == None because the equality 150 | # operator for result will expect None to be of its type. 151 | file.write("{}.{}({}) # {}, {}\n".format(slf, name, argsstr, callingframe[1], callingframe[2])) 152 | elif (isconst): 153 | file.write("assert({} == {}.{}({})) # {}, {}\n".format(retstr, slf, name, argsstr, callingframe[1], callingframe[2])) 154 | else: 155 | file.write("{} = {}.{}({}) # {}, {}\n".format(retstr, slf, name, argsstr, callingframe[1], callingframe[2])) 156 | 157 | intrack = intrack - 1 158 | return result 159 | return call 160 | 161 | 162 | def track_class(pathtohere, obj): 163 | pathtohere = pathtohere + "." + obj.__name__ 164 | 165 | if (obj.__name__ == 'ActionPlugin'): 166 | return 167 | 168 | for name in obj.__dict__: 169 | #if ((name[0] == "_") and (name != "__init__")): 170 | if ((name == "__del__") or (name == "__str__") or (name == "__repr__")): 171 | continue 172 | 173 | # if getattr is wrapped, something goes wrong when called from within __init__ 174 | # somewhere, I'm doing something with a not full constructed object. 175 | # I haven't taken the time to track it down. 176 | if (name == "__getattr__"): 177 | continue 178 | 179 | item = getattr(obj, name) 180 | if inspect.ismethod(item): 181 | setattr(obj, name, track_method(item, pathtohere, name)) 182 | 183 | 184 | 185 | module = pcbnew 186 | for name in dir(module): 187 | if (name[0] == "_"): 188 | continue 189 | 190 | obj = getattr(module, name) 191 | if inspect.isclass(obj): 192 | track_class(module.__name__, obj) 193 | 194 | elif (inspect.ismethod(obj) or inspect.isfunction(obj)): 195 | setattr(pcbnew, name, track_fn(obj, module.__name__ + "." + name)) 196 | 197 | elif inspect.isbuiltin(obj): 198 | pass 199 | 200 | else: 201 | #print("other {}".format(name)) 202 | pass 203 | 204 | def report_usage(): 205 | for k in stats: 206 | if (stats[k] == 0): 207 | continue; 208 | print("{} {}". format(k, stats[k])) 209 | -------------------------------------------------------------------------------- /ratnest/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | print("adding ratnest plugin") 3 | 4 | import pcbnew 5 | from . import ratnest 6 | 7 | class RatNestPlugin(pcbnew.ActionPlugin): 8 | def defaults(self): 9 | self.name = "Toggle Power Ratnest" 10 | self.category = "A descriptive category name" 11 | self.description = "This plugin turns off display of power ratsnest" 12 | 13 | def Run(self): 14 | # The entry function of the plugin that is executed on user action 15 | ratnest.TogglePowerRatnest() 16 | 17 | 18 | RatNestPlugin().register() # Instantiate and register to Pcbnew 19 | 20 | print("done adding ratnest") 21 | -------------------------------------------------------------------------------- /ratnest/ratnest.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import pcbnew 4 | 5 | powerratsoff = True 6 | 7 | def TogglePowerRatnest(): 8 | b = pcbnew.GetBoard() 9 | c = b.GetConnectivity() 10 | 11 | global powerratsoff 12 | print("setting power visibility to {}".format(powerratsoff)) 13 | for net in ['GND', '+6V', '-6V']: 14 | nc = b.GetNetcodeFromNetname(net) 15 | c.SetVisible(nc, powerratsoff) 16 | 17 | powerratsoff = not powerratsoff 18 | -------------------------------------------------------------------------------- /replicatelayout/replicatelayout.py: -------------------------------------------------------------------------------- 1 | import pdb 2 | import pcbnew 3 | 4 | board = pcbnew.GetBoard() 5 | 6 | # the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm) 7 | # the coordinate 121550000 corresponds to 121.550000 8 | 9 | SCALE = 1000000 10 | 11 | 12 | if hasattr(pcbnew, "LAYER_ID_COUNT"): 13 | pcbnew.PCB_LAYER_ID_COUNT = pcbnew.LAYER_ID_COUNT 14 | 15 | def coordsFromPolySet(ps): 16 | str = ps.Format() 17 | lines = str.split('\n') 18 | numpts = int(lines[2]) 19 | pts = [[int(n) for n in x.split(" ")] for x in lines[3:-2]] # -1 because of the extra two \n 20 | return pts 21 | 22 | def padsForNet(net): 23 | retval = [] 24 | for pad in board.GetPads(): 25 | # first get the netinfo, then get the netcode (int) 26 | if pad.GetNet().GetNet() == net: 27 | retval.append(pad) 28 | 29 | #for pad in retval: 30 | # print("pad {} connected to {}".format(pad.GetName(), board.GetNetsByNetcode()[net].GetNetname())); 31 | return retval 32 | 33 | from collections import defaultdict 34 | class SheetInstance: 35 | 36 | # "static" helper functions 37 | @staticmethod 38 | def GetSheetChildId(child): 39 | global depth_of_array 40 | path = child.GetPath().split('/') 41 | path.pop(0) # pop the empty head 42 | # the path will be missing if you have modules added 43 | # directly in pcbnew, not imported from eeschema netlist. 44 | if (len(path) == 0): 45 | return (None, None) 46 | # if there are multiple sheet heirarchies, where's the replication? 47 | # does the top cell contain an arrayed child? or is the child of top 48 | # arrayed. 49 | # if you have a sheet heirarchy like this: /58DED9F1/58F8C609/58F8CB4E 50 | # 58F8CB4E is the lowest child thing/package (ie not a sheet) 51 | # 58DED9F1 is the top child 52 | # 58F8C609 is the instance in the middle. 53 | sheetid = "/".join(path[0:-1]) 54 | childid = "/".join(path[-1:]) 55 | return (sheetid, childid) 56 | 57 | @staticmethod 58 | def GetNetCanonical(net): 59 | pads = [] 60 | for pad in padsForNet(net.GetNet()): 61 | sheetid, childid = SheetInstance.GetSheetChildId(pad.GetParent()) 62 | pads.append((childid, pad.GetPadName())) 63 | pads.sort() 64 | 65 | cname = "_".join([i[0]+":"+i[1] for i in pads]) 66 | #print("for net {} pads {}".format(net.GetNetname(), cname)) 67 | return cname 68 | 69 | @staticmethod 70 | def NetIsSheetInternal(net): 71 | commonsheet = None 72 | #print("for net " + net.GetNetname()) 73 | for pad in padsForNet(net.GetNet()): 74 | mod = pad.GetParent() 75 | sheetid, childid = SheetInstance.GetSheetChildId(mod) 76 | #print(" sheet {} child {} {}:{}".format(sheetid, str(childid), mod.GetReference(), pad.GetPadName())) 77 | if (childid == None): 78 | return None 79 | if commonsheet == None: 80 | commonsheet = sheetid 81 | if commonsheet != sheetid: 82 | return None 83 | return commonsheet 84 | 85 | @staticmethod 86 | def RegisterModulesAndNets(board): 87 | SheetInstance.__sheetinstances = {} 88 | SheetInstance.__child2sheetinstance = defaultdict(lambda: list()) 89 | for child in board.GetModules(): 90 | sheetid, childid = SheetInstance.GetSheetChildId(child) 91 | if (sheetid not in SheetInstance.__sheetinstances): 92 | SheetInstance.__sheetinstances[sheetid] = SheetInstance(sheetid) 93 | 94 | si = SheetInstance.__sheetinstances[sheetid] 95 | si.addChild(child) 96 | SheetInstance.__child2sheetinstance[childid].append(si) 97 | for net in board.GetNetsByNetcode().values(): 98 | common = SheetInstance.NetIsSheetInternal(net) 99 | if common == None: 100 | continue 101 | si = SheetInstance.__sheetinstances[common] 102 | si.addInternalNet(net) 103 | 104 | @staticmethod 105 | def GetSheetInstanceForModule(child): 106 | return SheetInstance.__sheetinstances[SheetInstance.GetSheetChildId(child)[0]] 107 | 108 | @staticmethod 109 | def GetSheetInstances(child): 110 | sheetid, childid = SheetInstance.GetSheetChildId(child) 111 | return SheetInstance.__child2sheetinstance[childid] 112 | 113 | # methods 114 | def __init__(self, id): 115 | self.id = id 116 | self.children = {} 117 | self.internalnets = {} 118 | 119 | def __str__(self): 120 | retval = "sheet id is :" + self.id + " {" 121 | retval += ", ".join([m.GetReference() for m in self.children.values()]) 122 | retval += "} internalnets: {" 123 | retval += ", ".join([n.GetNetname() for n in self.internalnets.values()]) 124 | return retval 125 | 126 | def addChild(self, child): 127 | sheetid,childid = SheetInstance.GetSheetChildId(child) 128 | if (childid == None): 129 | return 130 | self.children[childid] = child 131 | 132 | def addInternalNet(self, net): 133 | cannon = SheetInstance.GetNetCanonical(net) 134 | self.internalnets[cannon] = net 135 | 136 | def getChildById(self, id): 137 | return self.children[id] 138 | 139 | def getChildCorrespondingToModule(self, child): 140 | sheetid, childid = SheetInstance.GetSheetChildId(child) 141 | if (childid not in self.children): 142 | print("missing child {} others {}".format(mod.GetReference(), 143 | ", ".join([m.GetReference()+" "+id+" "+m.GetPath() for id,m in self.children.items()]))) 144 | return self.children[childid] 145 | 146 | def getChildren(self): 147 | return self.children.values() 148 | 149 | 150 | 151 | SheetInstance.RegisterModulesAndNets(board) 152 | 153 | # this trick came from here: http://stackoverflow.com/a/2669158 154 | import re 155 | tokenize = re.compile(r'(\d+)|(\D+)').findall 156 | def natural_sortkey(string): 157 | return tuple(int(num) if num else alpha for num, alpha in tokenize(string)) 158 | 159 | def replicate_sheet_trackst(fromnet, tonet, offset): 160 | board = tonet.GetParent() 161 | # remove tonet's old routing 162 | for track in board.TracksInNet(tonet.GetNet()): 163 | board.Remove(track) 164 | 165 | for track in board.TracksInNet(fromnet.GetNet()): 166 | if track.GetClass() == "VIA": 167 | # cloning is an easier way, but I want to ensure I 168 | # can create a Via from scratch 169 | #newvia = track.Clone() 170 | 171 | oldvia = board.GetViaByPosition(track.GetPosition()) 172 | newvia = pcbnew.VIA(board) 173 | # need to add before SetNet will work, so just doing it first 174 | board.Add(newvia) 175 | toplayer=-1 176 | bottomlayer=pcbnew.PCB_LAYER_ID_COUNT 177 | for l in range(pcbnew.PCB_LAYER_ID_COUNT): 178 | if not track.IsOnLayer(l): 179 | continue 180 | toplayer = max(toplayer, l) 181 | bottomlayer = min(bottomlayer, l) 182 | newvia.SetLayerPair(toplayer, bottomlayer) 183 | newvia.SetPosition(pcbnew.wxPoint(track.GetPosition().x+offset[0], 184 | track.GetPosition().y+offset[1])) 185 | newvia.SetViaType(oldvia.GetViaType()) 186 | newvia.SetWidth(oldvia.GetWidth()) 187 | newvia.SetNet(tonet) 188 | else: 189 | newtrack = pcbnew.TRACK(board) 190 | # need to add before SetNet will work, so just doing it first 191 | board.Add(newtrack) 192 | newtrack.SetStart(pcbnew.wxPoint(track.GetStart().x+offset[0], 193 | track.GetStart().y+offset[1])) 194 | newtrack.SetEnd(pcbnew.wxPoint(track.GetEnd().x+offset[0], 195 | track.GetEnd().y+offset[1])) 196 | newtrack.SetWidth(track.GetWidth()) 197 | newtrack.SetLayer(track.GetLayer()) 198 | 199 | newtrack.SetNet(tonet) 200 | 201 | fromzones = [] 202 | tozones = [] 203 | 204 | for zoneid in range(board.GetAreaCount()): 205 | zone = board.GetArea(zoneid) 206 | if (zone.GetNet().GetNetname() == fromnet.GetNetname()): 207 | fromzones.append(zone) 208 | continue; 209 | if (zone.GetNet().GetNetname() == tonet.GetNetname()): 210 | tozones.append(zone) 211 | continue; 212 | for zone in tozones: 213 | board.Remove(zone) 214 | 215 | for zone in fromzones: 216 | coords = coordsFromPolySet(zone.Outline()) 217 | #pdb.set_trace() 218 | newzone = board.InsertArea(tonet.GetNet(), 0, zone.GetLayer(), 219 | coords[0][0]+int(offset[0]), coords[0][1]+int(offset[1]), 220 | pcbnew.CPolyLine.DIAGONAL_EDGE) 221 | newoutline = newzone.Outline() 222 | for pt in coords[1:]: 223 | newoutline.Append(int(pt[0]+offset[0]), int(pt[1]+offset[1])) 224 | newzone.Hatch() 225 | 226 | 227 | def place_instances(mainref, pitch): 228 | 229 | pitch = (pitch[0] * SCALE, pitch[1] * SCALE) 230 | 231 | pivotmod = board.FindModuleByReference(mainref) 232 | 233 | sheetinstance = SheetInstance.GetSheetInstanceForModule(pivotmod) 234 | #peers = instances[pivotsheet] 235 | 236 | #print("getting for {}".format(pivotmod.GetReference())) 237 | arrayedsheets = sorted(SheetInstance.GetSheetInstances(pivotmod), 238 | key = lambda elt: natural_sortkey(elt.getChildCorrespondingToModule(pivotmod).GetReference())) 239 | #replicasheets = sorted(children[pivotinstance], key=lambda elt: natural_sortkey(elt[2])) 240 | 241 | 242 | print("children of the same instance as {}: {}".format(mainref, 243 | ",".join([m.GetReference() for m in sheetinstance.getChildren()]))) 244 | 245 | basepositions = {} 246 | for mod in sheetinstance.getChildren(): 247 | sheetid, childid = SheetInstance.GetSheetChildId(mod) 248 | basepositions[childid] = (mod.GetPosition().x, 249 | mod.GetPosition().y, 250 | mod.GetOrientation(), 251 | mod.IsFlipped()) 252 | 253 | print("basepositions {}".format(str(basepositions))) 254 | 255 | instnum = -1 256 | for i, si in enumerate(arrayedsheets): 257 | if (si.getChildCorrespondingToModule(pivotmod).GetReference() == mainref): 258 | instnum = i 259 | break 260 | 261 | print("{} is in index {}".format(mainref, instnum)) 262 | 263 | # we start with index=-instnum because we want the pivot module to stay where it is. 264 | for idx, si in enumerate(arrayedsheets, start=-instnum): 265 | #print("placing instance {} {}".format(idx, si.id)) 266 | if idx == 0: 267 | continue 268 | 269 | #first move the modules 270 | for peer in si.getChildren(): 271 | sheetid, childid = SheetInstance.GetSheetChildId(peer) 272 | newposition = basepositions[childid] 273 | newposition = (int(newposition[0] + idx*pitch[0]), 274 | int(newposition[1] + idx*pitch[1])) 275 | #print("moving peer {} to {},{}".format(peer.GetReference(), newposition[0], newposition[1])) 276 | if (peer.IsFlipped() != basepositions[childid][3]): 277 | peer.Flip(peer.GetPosition()) 278 | 279 | peer.SetPosition(pcbnew.wxPoint(*newposition)) 280 | peer.SetOrientation(basepositions[childid][2]) 281 | 282 | #copy the nets 283 | for fromnetid, fromnet in sheetinstance.internalnets.items(): 284 | if fromnetid not in si.internalnets: 285 | #print("{} is missing from {}\n".format(fromnetid, ", ".join(si.internalnets.keys()))) 286 | print("{} is missing\n".format(fromnetid)) 287 | continue 288 | 289 | tonet = si.internalnets[fromnetid] 290 | #print("copying net {} to {}".format(fromnet.GetNetname(), tonet.GetNetname())) 291 | replicate_sheet_trackst(fromnet, tonet, (idx*pitch[0],idx*pitch[1])) 292 | 293 | 294 | 295 | #place_instances("U7", (0, -45)) 296 | #place_instances("U71", (0, -45)) 297 | 298 | place_instances("J8", (30, 0)) 299 | place_instances("U7", (30, 0)) 300 | 301 | # In the future, this build connectivity call will not be neccessary. 302 | # I have submitted a patch to include this in the code for Refresh. 303 | # You'll know you needed it if pcbnew crashes without it. 304 | board.BuildConnectivity() 305 | 306 | pcbnew.Refresh(); 307 | 308 | 309 | -------------------------------------------------------------------------------- /save_config/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /save_config/save_config.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import pcbnew 4 | import numbers 5 | import xml.etree.ElementTree as ET 6 | import xml.dom.minidom as MD 7 | import os 8 | import os.path 9 | import pdb 10 | import re 11 | import pprint 12 | 13 | 14 | def GetConfigPath(): 15 | configpath = pcbnew.GetKicadConfigPath() 16 | return configpath + "/kicad_mmccoo.xml" 17 | 18 | def GetConfigTree(): 19 | 20 | path = GetConfigPath() 21 | if os.path.isfile(path): 22 | tree = ET.parse(path) 23 | root = tree.getroot() 24 | return root 25 | 26 | root = ET.fromstring("") 27 | return root 28 | 29 | def GetHierElement(root, path): 30 | elt = root 31 | for name in path.split('/'): 32 | sub = elt.find(name) 33 | if (not sub): 34 | sub = ET.SubElement(elt, name) 35 | 36 | elt = sub 37 | 38 | def Save(root): 39 | rough_string = ET.tostring(root, 'utf-8') 40 | 41 | reparsed = MD.parseString(rough_string) 42 | pretty = reparsed.toprettyxml(indent=" ") 43 | 44 | # remove empty lines. Got it from here: 45 | # https://stackoverflow.com/a/1140966/23630 46 | pretty = os.linesep.join([s for s in pretty.splitlines() if s and not s.isspace()]) 47 | 48 | path = GetConfigPath() 49 | with open(path, "w") as text_file: 50 | text_file.write(pretty) 51 | 52 | 53 | 54 | def SaveConfig(name, value): 55 | root = GetConfigTree() 56 | 57 | child = root.find(name) 58 | 59 | if (child == None): 60 | child = ET.SubElement(root, name) 61 | 62 | child.text = value 63 | 64 | Save(root) 65 | 66 | def GetConfig(name, default=None): 67 | tree = GetConfigTree() 68 | 69 | child = tree.find(name) 70 | if (child == None): 71 | return default 72 | return child.text; 73 | 74 | def ValueToElt(parent, value): 75 | if isinstance(value, dict): 76 | d = ET.SubElement(parent, 'dict') 77 | for key in value: 78 | sub = ValueToElt(d, value[key]) 79 | sub.attrib['key'] = str(key) 80 | return d 81 | 82 | if isinstance(value, list): 83 | l = ET.SubElement(parent, 'list') 84 | for elt in value: 85 | sub = ValueToElt(l, elt) 86 | return l 87 | 88 | if isinstance(value, tuple): 89 | l = ET.SubElement(parent, 'tuple') 90 | for elt in value: 91 | sub = ValueToElt(l, elt) 92 | return l 93 | 94 | elif isinstance(value, basestring): 95 | s = ET.SubElement(parent, "string") 96 | s.text = value 97 | 98 | return s 99 | 100 | elif isinstance(value, numbers.Number): 101 | n = ET.SubElement(parent, "number") 102 | n.text = str(value) 103 | return n 104 | 105 | 106 | else: 107 | return None 108 | 109 | def EltToValue(elt): 110 | 111 | if elt == None: 112 | return None 113 | 114 | if elt.tag == "list": 115 | retval = [] 116 | for child in elt: 117 | retval.append(EltToValue(child)) 118 | return retval 119 | 120 | if elt.tag == "tuple": 121 | retval = [] 122 | for child in elt: 123 | retval.append(EltToValue(child)) 124 | return tuple(retval) 125 | 126 | if elt.tag == "dict": 127 | retval = {} 128 | for child in elt: 129 | retval[child.attrib['key']] = EltToValue(child) 130 | return retval 131 | 132 | if elt.tag == "string": 133 | return elt.text 134 | 135 | if elt.tag == "number": 136 | return float(elt.text) 137 | 138 | 139 | 140 | return None 141 | 142 | 143 | 144 | 145 | def SaveConfigComplex(name, value): 146 | root = GetConfigTree() 147 | 148 | child = root.find(name) 149 | 150 | if (child == None): 151 | child = ET.SubElement(root, name) 152 | 153 | child.clear() 154 | ValueToElt(child, value) 155 | 156 | Save(root) 157 | 158 | def GetConfigComplex(name, default=None): 159 | tree = GetConfigTree() 160 | 161 | child = tree.find(name) 162 | if (child == None): 163 | return default 164 | 165 | return EltToValue(list(child)[0]) 166 | 167 | if __name__ == "__main__": 168 | root = GetConfigTree() 169 | 170 | m = [ 171 | { 'size': 1.2, 172 | 'lib': "lib1", 173 | 'foot': "mh1.2" 174 | }, 175 | { 'size': 1.3, 176 | 'lib': "lib1", 177 | 'foot': "mh1.3" 178 | }, 179 | { 'size': 1.4, 180 | 'lib': "lib1", 181 | 'foot': "mh1.4" 182 | }] 183 | 184 | SaveConfigComplex("complex", m) 185 | pprint.pprint(GetConfigComplex("complex")) 186 | 187 | print(GetConfig("test1")) 188 | SaveConfig("test2", "this is the string") 189 | print(GetConfig("test2")) 190 | SaveConfig("test2.path", "this is the other string") 191 | print(GetConfig("test2.path")) 192 | -------------------------------------------------------------------------------- /simpledialog/DialogUtils.py: -------------------------------------------------------------------------------- 1 | 2 | import pcbnew 3 | import wx 4 | import ntpath 5 | import pdb 6 | import os 7 | from sets import Set 8 | 9 | from ..save_config import save_config 10 | 11 | 12 | # seems like this class shouldn't be necessary. It just creates a basic dialog with 13 | # ok and cancel buttons. Very common. Surely, there is already such a class. 14 | # This class gives a basic dialog with ok and cancel buttons. 15 | class BaseDialog(wx.Dialog): 16 | def __init__(self, dialogname, onok=None): 17 | pcbnew_frame = \ 18 | filter(lambda w: w.GetTitle().startswith('Pcbnew'), wx.GetTopLevelWindows())[0] 19 | 20 | wx.Dialog.__init__(self, pcbnew_frame, 21 | id=wx.ID_ANY, 22 | title=dialogname, 23 | pos=wx.DefaultPosition) 24 | 25 | self.ok_cbs = [] 26 | if onok: 27 | self.ok_cbs.append(onok) 28 | 29 | self.main_sizer = wx.BoxSizer(wx.VERTICAL) 30 | self.SetSizer(self.main_sizer) 31 | 32 | 33 | # add ok and cancel buttons. 34 | sizer_ok_cancel = wx.BoxSizer(wx.HORIZONTAL) 35 | self.main_sizer.Add(item=sizer_ok_cancel, proportion=0, flag=wx.EXPAND) 36 | 37 | ok_button = wx.Button(self, wx.ID_OK, u"OK", wx.DefaultPosition, wx.DefaultSize, 0) 38 | ok_button.SetDefault() 39 | sizer_ok_cancel.Add(item=ok_button, proportion=1) #, flag=wx.ALL, border=5) 40 | ok_button.Bind(wx.EVT_BUTTON, self.OnOK) 41 | 42 | cancel_button = wx.Button(self, wx.ID_CANCEL, u"Cancel", wx.DefaultPosition, wx.DefaultSize, 0) 43 | sizer_ok_cancel.Add(cancel_button, 1) #, wx.ALL, 5) 44 | 45 | 46 | self.ok_cancel_sizer_index = 0 47 | #self.Layout() 48 | self.Centre(wx.BOTH) 49 | 50 | # when the user clicks ok, we want to save stuff like directories used.... 51 | def OnOK(self, event): 52 | for cb in self.ok_cbs: 53 | cb() 54 | event.Skip() 55 | 56 | # wx.EXPAND is important for scrolled children. 57 | def Add(self, item, proportion=0, flag=wx.EXPAND|wx.ALL, border=0, origitem=None): 58 | self.main_sizer.Insert(item=item, 59 | before=self.ok_cancel_sizer_index, 60 | flag = flag, 61 | proportion=proportion, 62 | border=border) 63 | 64 | self.ok_cancel_sizer_index = self.ok_cancel_sizer_index+1 65 | self.main_sizer.Layout() 66 | self.Layout() 67 | 68 | ok_cb = getattr(item, "OnOKCB", None) 69 | if(not ok_cb): 70 | ok_cb = getattr(origitem, "OnOKCB", None) 71 | if (callable(ok_cb)): 72 | self.ok_cbs.append(ok_cb) 73 | 74 | # wx.EXPAND is important for scrolled children. 75 | def AddLabeled(self, item, label, proportion=0, flag=wx.EXPAND|wx.ALL, border=0): 76 | # Add() above assumes wx.StaticBoxSizer is used. Remember that if deciding 77 | # not to use StaticBox here. 78 | static = wx.StaticBox(self, wx.ID_ANY, label) 79 | staticsizer = wx.StaticBoxSizer(static, wx.VERTICAL) 80 | # the default color is very lite on my linux system. 81 | # https://stackoverflow.com/a/21112377/23630 82 | static.SetBackgroundColour((150, 150, 150)) 83 | 84 | item.Reparent(static) 85 | # do I really want to pass these arguments to both adds? 86 | staticsizer.Add(item, proportion=proportion, flag=flag, border=border) 87 | 88 | self.Add(staticsizer, proportion=proportion, flag=flag, border=border, origitem=item) 89 | 90 | def IncSize(self, width=0, height=0): 91 | # need a little extra size for the scrolled layers list. 92 | # based on this hint: 93 | # https://groups.google.com/forum/#!topic/wxpython-users/7zUmbnA3rGk 94 | fontsz = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT).GetPixelSize() 95 | size = self.GetSizer().CalcMin() 96 | size.height += fontsz.y*height 97 | size.width += fontsz.x*width 98 | self.SetClientSize(size) 99 | 100 | 101 | class ScrolledPicker(wx.Window): 102 | def __init__(self, parent, singleton=True, cols=1): 103 | wx.Window.__init__(self, parent, wx.ID_ANY) 104 | 105 | self.singleton = singleton 106 | 107 | self.boxes = [] 108 | 109 | self.sizer = wx.BoxSizer(wx.VERTICAL) 110 | self.SetSizer(self.sizer) 111 | 112 | 113 | self.buttonwin = wx.Window(self) 114 | self.buttonsizer = wx.BoxSizer(wx.HORIZONTAL) 115 | self.buttonwin.SetSizer(self.buttonsizer) 116 | self.sizer.Add(self.buttonwin) 117 | 118 | if (not singleton): 119 | self.value = Set() 120 | 121 | self.selectall = wx.Button(self.buttonwin, label="select all"); 122 | self.selectall.Bind(wx.EVT_BUTTON, self.OnSelectAllNone) 123 | self.buttonsizer.Add(self.selectall) 124 | 125 | self.selectnone = wx.Button(self.buttonwin, label="select none"); 126 | self.selectnone.Bind(wx.EVT_BUTTON, self.OnSelectAllNone) 127 | self.buttonsizer.Add(self.selectnone) 128 | 129 | self.scrolled = wx.ScrolledWindow(self, wx.ID_ANY) 130 | self.sizer.Add(self.scrolled, proportion=1, flag=wx.EXPAND|wx.ALL) 131 | 132 | fontsz = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT).GetPixelSize() 133 | self.scrolled.SetScrollRate(fontsz.x, fontsz.y) 134 | 135 | self.scrollsizer = wx.GridSizer(cols=cols, hgap=5, vgap=5) 136 | self.scrolled.SetSizer(self.scrollsizer) 137 | 138 | def Add(self, w): 139 | w.Reparent(self.scrolled) 140 | self.scrollsizer.Add(w) 141 | if (isinstance(w, wx.CheckBox) or isinstance(w, wx.RadioButton)): 142 | self.boxes.append(w) 143 | 144 | def AddSelector(self, name, binding=None): 145 | if (binding == None): 146 | binding = self.OnButton 147 | 148 | if (not self.singleton): 149 | rb = wx.CheckBox(self, label=name) 150 | rb.Bind(wx.EVT_CHECKBOX, binding) 151 | elif (len(self.boxes) == 0): 152 | # boxes gets updated in Add 153 | rb = wx.RadioButton(self.scrolled, label=name, style=wx.RB_GROUP) 154 | rb.Bind(wx.EVT_RADIOBUTTON, binding) 155 | self.SendSelectorEvent(rb) 156 | else: 157 | rb = wx.RadioButton(self.scrolled, label=name) 158 | rb.Bind(wx.EVT_RADIOBUTTON, binding) 159 | 160 | self.Add(rb) 161 | 162 | def OnButton(self, event): 163 | value = event.EventObject.GetLabel() 164 | 165 | if (self.singleton): 166 | self.value = value 167 | else: 168 | if (event.EventObject.IsChecked()): 169 | self.value.add(value) 170 | else: 171 | self.value.remove(value) 172 | 173 | 174 | def SendSelectorEvent(self, box): 175 | if (isinstance(box, wx.CheckBox)): 176 | # I have the feeling that this is the wrong way to trigger 177 | # an event. 178 | newevent = wx.CommandEvent(wx.EVT_CHECKBOX.evtType[0]) 179 | newevent.SetEventObject(box) 180 | wx.PostEvent(box, newevent) 181 | 182 | if (isinstance(box, wx.RadioButton)): 183 | newevent = wx.CommandEvent(wx.EVT_RADIOBUTTON.evtType[0]) 184 | newevent.SetEventObject(box) 185 | wx.PostEvent(box, newevent) 186 | 187 | 188 | def Clear(self): 189 | #self.scrollsizer.Clear() 190 | self.scrolled.DestroyChildren() 191 | self.boxes = [] 192 | 193 | def OnSelectAllNone(self, event): 194 | newvalue = True 195 | if (event.EventObject == self.selectnone): 196 | newvalue = False 197 | 198 | for box in self.boxes: 199 | box.SetValue(newvalue) 200 | self.SendSelectorEvent(box) 201 | 202 | class FilePicker(wx.Window): 203 | def __init__(self, parent, value=None, wildcard=None, configname=None): 204 | wx.Window.__init__(self, parent) 205 | 206 | self.configname = configname 207 | if (value == None): 208 | value = os.path.expanduser("~") 209 | 210 | if (self.configname != None): 211 | value = save_config.GetConfig(self.configname + ".path", value) 212 | 213 | self.value = value 214 | self.wildcard = wildcard 215 | 216 | sizer = wx.BoxSizer(wx.HORIZONTAL) 217 | self.SetSizer(sizer) 218 | 219 | self.selectedfile = wx.TextCtrl(parent=self, 220 | id=wx.ID_ANY, 221 | value=self.value, 222 | size=(490,25)) 223 | self.selectedfile.Bind(wx.EVT_TEXT, self.OnText) 224 | sizer.Add(self.selectedfile, proportion=1) 225 | 226 | self.browse = wx.Button(self, label="browse"); 227 | self.browse.Bind(wx.EVT_BUTTON, self.OnButton) 228 | 229 | sizer.Add(self.browse, proportion=0) 230 | 231 | def OnText(self, event): 232 | self.value = self.selectedfile.GetValue() 233 | 234 | 235 | def OnButton(self, event): 236 | fileDialog = wx.FileDialog(self, "open xyz", 237 | defaultDir=ntpath.dirname(self.value), 238 | defaultFile=ntpath.basename(self.value), 239 | style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST) 240 | if (self.wildcard != None): 241 | fileDialog.SetWildcard(self.wildcard) 242 | if fileDialog.ShowModal() == wx.ID_CANCEL: 243 | return 244 | self.value = fileDialog.GetPath() 245 | self.selectedfile.SetValue(self.value) 246 | 247 | def OnOKCB(self): 248 | if (self.configname != None): 249 | save_config.SaveConfig(self.configname + ".path", self.value) 250 | 251 | 252 | 253 | class BasicLayerPicker(wx.Window): 254 | def __init__(self, parent, layers=None, cols=4): 255 | wx.Window.__init__(self, parent, wx.ID_ANY) 256 | 257 | if (layers == None): 258 | layers = ['F.Cu', 'F.Silks','Edge.Cuts', 'F.Mask', 259 | 'B.Cu', 'B.SilkS','Cmts.User', 'B.Mask'] 260 | 261 | sizer = wx.GridSizer(cols=cols)#, hgap=5, vgap=5) 262 | self.SetSizer(sizer) 263 | 264 | board = pcbnew.GetBoard() 265 | self.layertable = {} 266 | numlayers = pcbnew.PCB_LAYER_ID_COUNT 267 | for i in range(numlayers): 268 | self.layertable[board.GetLayerName(i)] = i 269 | 270 | for layername in layers: 271 | if (layername not in self.layertable): 272 | ValueError("layer {} doesn't exist".format(layername)) 273 | 274 | if (layername == layers[0]): 275 | rb = wx.RadioButton(self, label=layername, style=wx.RB_GROUP) 276 | self.value = layername 277 | self.valueint = self.layertable[layername] 278 | else: 279 | rb = wx.RadioButton(self, label=layername) 280 | rb.Bind(wx.EVT_RADIOBUTTON, self.OnButton) 281 | sizer.Add(rb) 282 | 283 | def OnButton(self, event): 284 | self.value = event.EventObject.GetLabel() 285 | self.valueint = self.layertable[self.value] 286 | 287 | # when adding an instance of this class to a sizer, it's really important 288 | # to pass the flag=wx.EXPAND 289 | class AllLayerPicker(ScrolledPicker): 290 | def __init__(self, parent, singleton=True): 291 | numlayers = pcbnew.PCB_LAYER_ID_COUNT 292 | 293 | ScrolledPicker.__init__(self, parent, singleton=singleton, cols=4) 294 | 295 | for i in range(numlayers): 296 | layername = pcbnew.GetBoard().GetLayerName(i) 297 | self.AddSelector(layername) 298 | 299 | 300 | 301 | class ModulePicker(ScrolledPicker): 302 | def __init__(self, parent, singleton=True): 303 | ScrolledPicker.__init__(self, parent, singleton=singleton, cols=4) 304 | 305 | if (not self.singleton): 306 | self.value = Set() 307 | 308 | self.board = pcbnew.GetBoard() 309 | modnames = [mod.GetReference() for mod in self.board.GetModules()] 310 | modnames.sort() 311 | 312 | for mod in modnames: 313 | self.AddSelector(mod) 314 | 315 | 316 | class NetPicker(ScrolledPicker): 317 | def __init__(self, parent, singleton=True): 318 | ScrolledPicker.__init__(self, parent, singleton=singleton, cols=4) 319 | 320 | self.board = pcbnew.GetBoard() 321 | nets = [str(net) for net in self.board.GetNetsByName().keys() if (str(net) != "")] 322 | nets.sort() 323 | 324 | for net in nets: 325 | self.AddSelector(net) 326 | 327 | def GetValuePtr(self): 328 | nbn = self.board.GetNetsByName() 329 | if (self.singleton): 330 | return nbn[self.value] 331 | 332 | retval = [] 333 | for net in self.value: 334 | retval.append(nbn[net]) 335 | 336 | return retval 337 | 338 | class FootprintDialog(BaseDialog): 339 | def __init__(self): 340 | super(FootprintDialog, self).__init__("simple dialog") 341 | 342 | # GetLogicalLibs gives wxStrings 343 | libnames = [str(s) for s in pcbnew.GetLogicalLibs()] 344 | 345 | self.libpicker = ScrolledPicker(self, cols=4) 346 | 347 | self.libpicker.value = libnames[0] 348 | for lib in libnames: 349 | self.libpicker.AddSelector(lib, self.OnLibButton) 350 | 351 | self.AddLabeled(item=self.libpicker, 352 | label="Select a Library:", 353 | proportion=1, 354 | flag=wx.EXPAND|wx.ALL, 355 | border=2) 356 | 357 | # this is to force the dialog to be wide enough to show all the libraries 358 | libx, liby = self.libpicker.scrollsizer.ComputeFittingWindowSize(self) 359 | self.SetMinSize(wx.Size(libx, -1)) 360 | 361 | self.modpicker = ScrolledPicker(self, cols=2) 362 | self.AddLabeled(item=self.modpicker, 363 | label="Select a Module:", 364 | proportion=1, 365 | flag=wx.EXPAND|wx.ALL, 366 | border=2) 367 | self.SetLib() 368 | 369 | 370 | self.IncSize(25,15) 371 | 372 | 373 | def OnLibButton(self, event): 374 | self.libpicker.value = event.EventObject.GetLabel() 375 | self.SetLib(); 376 | 377 | 378 | def SetLib(self): 379 | self.modpicker.Clear() 380 | 381 | mods = pcbnew.FootprintsInLib(self.libpicker.value) 382 | for mod in mods: 383 | self.modpicker.AddSelector(mod) 384 | 385 | 386 | self.Layout() 387 | -------------------------------------------------------------------------------- /simpledialog/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /simpledialog/simpledialog.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import wx 4 | import wx.lib 5 | import wx.grid 6 | import pcbnew 7 | import ntpath 8 | import sys, os.path, inspect 9 | from sets import Set 10 | 11 | oldpath = sys.path 12 | # inspect.stack()[0][1] is the full path to the current file. 13 | sys.path.insert(0, os.path.dirname(inspect.stack()[0][1])) 14 | import DialogUtils 15 | sys.path = oldpath 16 | 17 | class SimpleDialog(DialogUtils.BaseDialog): 18 | def __init__(self): 19 | super(SimpleDialog, self).__init__("simple dialog") 20 | 21 | text = wx.StaticText(self, wx.ID_ANY, u"Select the Input File", 22 | wx.DefaultPosition, wx.DefaultSize, 0) 23 | text.Wrap(-1) 24 | 25 | # scrolltest = DialogUtils.ScrolledPicker(self) 26 | # for i in range(10): 27 | # scrolltest.Add(wx.TextCtrl(parent=scrolltest, value='here' + str(i))); 28 | # self.AddLabeled(item=scrolltest, label="scrolled", proportion=1, border=2) 29 | 30 | # # https://wxpython.org/Phoenix/docs/html/wx.Sizer.html#wx.Sizer.Add 31 | # # https://wxpython.org/Phoenix/docs/html/wx.Sizer.html#wx-sizer 32 | # self.Add(item=text, proportion=0, flag=wx.ALL, border=5) 33 | 34 | # self.file_picker = DialogUtils.FilePicker(self, "/home/mmccoo/kicad/kicad_mmccoo/simpledialog/simpledialog.py") 35 | # self.Add(item=self.file_picker, proportion=0, flag=wx.ALL, border=5) 36 | 37 | # self.file_picker2 = DialogUtils.FilePicker(self, "/home/mmccoo/kicad/kicad_mmccoo/simpledialog/simpledialog.py") 38 | # self.AddLabeled(item=self.file_picker2, label="this is the label", 39 | # proportion=0, flag=wx.ALL, border=2) 40 | 41 | 42 | # self.basic_layer = DialogUtils.BasicLayerPicker(self) 43 | # self.AddLabeled(item=self.basic_layer, label="basic label", border=2) 44 | 45 | # self.basic_layer2 = DialogUtils.BasicLayerPicker(self) 46 | # self.Add(item=self.basic_layer2, flag=wx.EXPAND) 47 | 48 | # self.nets = DialogUtils.NetPicker(self, singleton=False) 49 | # self.AddLabeled(item=self.nets, 50 | # label="all nets", 51 | # proportion=1, 52 | # flag=wx.EXPAND|wx.ALL, 53 | # border=2) 54 | 55 | # self.mods = DialogUtils.ModulePicker(self, singleton=False) 56 | # self.AddLabeled(item=self.mods, 57 | # label="all mods", 58 | # proportion=1, 59 | # flag=wx.EXPAND|wx.ALL, 60 | # border=2) 61 | 62 | 63 | # self.all_layer = DialogUtils.AllLayerPicker(self) 64 | # self.AddLabeled(item=self.all_layer, 65 | # label="all layers", 66 | # proportion=1, 67 | # flag=wx.EXPAND|wx.ALL, 68 | # border=2) 69 | 70 | 71 | self.IncSize(height=10) 72 | 73 | #dlg = SimpleDialog() 74 | dlg = DialogUtils.FootprintDialog() 75 | res = dlg.ShowModal() 76 | 77 | print("lib {} footprint {}".format(dlg.libpicker.value, dlg.modpicker.value)) 78 | 79 | print("nets {}".format(dlg.nets.value)) 80 | print("mods {}".format(dlg.mods.value)) 81 | #print("file {}".format(dlg.file_picker.filename)) 82 | #print("basic layer {}".format(dlg.basic_layer.value)) 83 | if res == wx.ID_OK: 84 | print("ok") 85 | else: 86 | print("cancel") 87 | -------------------------------------------------------------------------------- /simplegui/simplegui.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | # sudo apt install wxglade for gui builder 4 | # https://wiki.wxpython.org/AnotherTutorial 5 | import wx 6 | import pcbnew 7 | 8 | 9 | class SimpleGui(wx.Frame): 10 | def __init__(self, parent, board): 11 | wx.Frame.__init__(self, parent, title="this is the title") 12 | self.panel = wx.Panel(self) 13 | label = wx.StaticText(self.panel, label = "Hello World") 14 | button = wx.Button(self.panel, label="Button label", id=1) 15 | 16 | nets = board.GetNetsByName() 17 | self.netnames = [] 18 | for netname, net in nets.items(): 19 | if (str(netname) == ""): 20 | continue 21 | self.netnames.append(str(netname)) 22 | 23 | netcb = wx.ComboBox(self.panel, choices=self.netnames) 24 | netcb.SetSelection(0) 25 | 26 | netsbox = wx.BoxSizer(wx.HORIZONTAL) 27 | netsbox.Add(wx.StaticText(self.panel, label="Nets:")) 28 | netsbox.Add(netcb, proportion=1) 29 | 30 | modules = board.GetModules() 31 | self.modulenames = [] 32 | for mod in modules: 33 | self.modulenames.append("{}({})".format(mod.GetReference(), mod.GetValue())) 34 | modcb = wx.ComboBox(self.panel, choices=self.modulenames) 35 | modcb.SetSelection(0) 36 | 37 | modsbox = wx.BoxSizer(wx.HORIZONTAL) 38 | modsbox.Add(wx.StaticText(self.panel, label="Modules:")) 39 | modsbox.Add(modcb, proportion=1) 40 | 41 | box = wx.BoxSizer(wx.VERTICAL) 42 | box.Add(label, proportion=0) 43 | box.Add(button, proportion=0) 44 | box.Add(netsbox, proportion=0) 45 | box.Add(modsbox, proportion=0) 46 | 47 | self.panel.SetSizer(box) 48 | self.Bind(wx.EVT_BUTTON, self.OnPress, id=1) 49 | self.Bind(wx.EVT_COMBOBOX, self.OnSelectNet, id=netcb.GetId()) 50 | self.Bind(wx.EVT_COMBOBOX, self.OnSelectMod, id=modcb.GetId()) 51 | 52 | def OnPress(self, event): 53 | print("in OnPress") 54 | 55 | def OnSelectNet(self, event): 56 | item = event.GetSelection() 57 | print("Net {} was selected".format(self.netnames[item])) 58 | 59 | def OnSelectMod(self, event): 60 | item = event.GetSelection() 61 | print("Module {} was selected".format(self.modulenames[item])) 62 | 63 | def InitSimpleGui(board): 64 | sg = SimpleGui(None, board) 65 | sg.Show(True) 66 | return sg 67 | 68 | 69 | sg = InitSimpleGui(pcbnew.GetBoard()) 70 | -------------------------------------------------------------------------------- /svg2border/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import svg2border 3 | -------------------------------------------------------------------------------- /svg2border/drawing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 61 | 66 | 67 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /svg2border/parse_svg_path.py: -------------------------------------------------------------------------------- 1 | 2 | import xml.etree.ElementTree as ET 3 | import re 4 | 5 | 6 | 7 | # paths are explained here: 8 | # https://www.w3schools.com/graphics/svg_path.asp 9 | # sample (the letter a): 10 | # m is move to 11 | # q is a quadatric bezier curve 12 | # z is close path 13 | # d = m 161.0332,166.95004 14 | # q -4.35547,0 -6.03515,0.9961 -1.67969,0.99609 -1.67969,3.39844 0,1.91406 15 | # 1.25,3.04687 1.26953,1.11328 3.4375,1.11328 2.98828,0 4.78516,-2.10937 16 | # 1.8164,-2.12891 1.8164,-5.64453 l 0,-0.80079 -3.57422,0 z m 17 | # 7.16797,-1.48437 0,12.48047 -3.59375,0 0,-3.32031 q -1.23047,1.99218 18 | # -3.0664,2.94921 -1.83594,0.9375 -4.49219,0.9375 -3.35938,0 19 | # -5.35156,-1.875 -1.97266,-1.89453 -1.97266,-5.05859 0,-3.69141 20 | # 2.46094,-5.56641 2.48047,-1.875 7.38281,-1.875 l 5.03906,0 21 | # 0,-0.35156 q 0,-2.48047 -1.64062,-3.82812 -1.6211,-1.36719 22 | # -4.57032,-1.36719 -1.875,0 -3.65234,0.44922 -1.77734,0.44922 23 | # -3.41797,1.34765 l 0,-3.32031 q 1.97266,-0.76172 3.82813,-1.13281 24 | # 1.85547,-0.39063 3.61328,-0.39063 4.74609,0 7.08984,2.46094 25 | # 2.34375,2.46094 2.34375,7.46094 z 26 | 27 | # but that has bezier curvers which kicad doesn't support 28 | # do as described here to get segments 29 | # http://www.inkscapeforum.com/viewtopic.php?t=4308 30 | 31 | # the same a (though choppy) looks like this: 32 | # ) 89 | # This transform definition specifies a transformation in the form of 90 | # a transformation matrix of six values. 91 | # matrix(a,b,c,d,e,f) is equivalent to applying the transformation matrix 92 | # ( a c e 93 | # b d f 94 | # 0 0 1 ) 95 | # which maps coordinates from a previous coordinate system into a 96 | # new coordinate system by the following matrix equalities: 97 | # (xnewCoordSys ) = ( a c e) ( xprevCoordSys ) = ( axprevCoordSys+cyprevCoordSys+e ) 98 | # ynewCoordSys b d f yprevCoordSys bxprevCoordSys+dyprevCoordSys+f 99 | # 1 0 0 1 1 ) 1 100 | 101 | newx = self.trans[0] * pt[0] + self.trans[2] * pt[1] + self.trans[4] 102 | newy = self.trans[1] * pt[0] + self.trans[3] * pt[1] + self.trans[5] 103 | 104 | return (newx, newy) 105 | 106 | def append_pt(self, pt): 107 | self.curpoly.append(self.transform_pt(pt)) 108 | 109 | def parse_path_string(self, d, trans): 110 | self.trans = trans 111 | 112 | self.polys = [] 113 | self.curpoly = None 114 | 115 | firstloc = None 116 | curloc = None 117 | curcmd = None 118 | 119 | self.d = d.lstrip(" \t\n,") 120 | 121 | while self.d: 122 | # get_cmd returns passed argument as fallback 123 | cmd = self.get_cmd(curcmd) 124 | 125 | if (cmd == 'M'): 126 | curloc = self.get_pt() 127 | if (not firstloc): 128 | firstloc = curloc 129 | if (not self.curpoly): 130 | self.curpoly = [] 131 | self.polys.append(self.curpoly) 132 | self.append_pt(curloc) 133 | curcmd = 'L' 134 | 135 | elif (cmd == 'm'): 136 | pt = self.get_pt() 137 | if (curloc): 138 | curloc = self.pt_add(curloc, pt) 139 | else: 140 | # the first m of a path is treated as absolute 141 | curloc = pt 142 | if (not firstloc): 143 | firstloc = curloc 144 | if (not self.curpoly): 145 | self.curpoly = [] 146 | self.polys.append(self.curpoly) 147 | self.append_pt(curloc) 148 | curcmd = 'l' 149 | 150 | elif (cmd == 'l'): 151 | pt = self.get_pt() 152 | curloc = self.pt_add(curloc, pt) 153 | self.append_pt(curloc) 154 | curcmd = 'l' 155 | 156 | elif (cmd == 'L'): 157 | curloc = self.get_pt() 158 | self.append_pt(curloc) 159 | curcmd = 'L' 160 | 161 | elif (cmd == 'z') or (cmd == 'Z'): 162 | self.append_pt(firstloc) 163 | self.curpoly = None 164 | curloc = firstloc 165 | firstloc = None 166 | 167 | # http://www.ariel.com.au/a/python-point-int-poly.html 168 | # determine if a point is inside a given polygon or not 169 | # Polygon is a list of (x,y) pairs. 170 | @staticmethod 171 | def point_inside_polygon(x,y,poly): 172 | 173 | n = len(poly) 174 | inside =False 175 | 176 | p1x,p1y = poly[0] 177 | for i in range(n+1): 178 | p2x,p2y = poly[i % n] 179 | if y > min(p1y,p2y): 180 | if y <= max(p1y,p2y): 181 | if x <= max(p1x,p2x): 182 | if p1y != p2y: 183 | xinters = (y-p1y)*(p2x-p1x)/(p2y-p1y)+p1x 184 | if p1x == p2x or x <= xinters: 185 | inside = not inside 186 | p1x,p1y = p2x,p2y 187 | 188 | return inside 189 | 190 | def group_by_bound_and_holes(self): 191 | # inkscape gives me a path by first listing the boundaries and then the holes. 192 | # I want to know a boundary and its holes, then another boundary,... 193 | # this function will reorder the polys that way. 194 | 195 | bounds = [] 196 | holes = [] 197 | for poly in self.polys: 198 | if not poly_is_hole(poly): 199 | bounds.append(poly) 200 | else: 201 | holes.append(poly) 202 | 203 | # a lookup table keyed off the boundary index. 204 | # the values are lists of holes 205 | bound_holes = {} 206 | for bi, bound in enumerate(bounds): 207 | for hi, hole in enumerate(holes): 208 | # pass x and y of the first point of hole. 209 | if SVGPath.point_inside_polygon(hole[0][0], hole[0][1], bound): 210 | if bi not in bound_holes: 211 | bound_holes[bi] = [] 212 | bound_holes[bi].append(hole) 213 | 214 | retshapes = [] 215 | for bi, bound in enumerate(bounds): 216 | if (bi in bound_holes): 217 | retshapes.append(SVGShape(bound, bound_holes[bi])) 218 | else: 219 | retshapes.append(SVGShape(bound, [])) 220 | 221 | return retshapes 222 | 223 | def __init__(self, d, trans): 224 | self.origd = d 225 | 226 | self.parse_path_string(d,trans) 227 | 228 | 229 | # this site was helpful in debugging 230 | # http://www.bluebit.gr/matrix-calculator/multiply.aspx 231 | def multiply_transforms(a, b): 232 | # this assumes a and b are represented with these indexes 233 | # 0 2 4 <- these are indexes of a and b 234 | # 1 3 5 235 | # "0" "0" "1" <- these always have the value 0 0 1 236 | retval = [ 237 | a[0]*b[0]+a[2]*b[1], # 0 238 | a[1]*b[0]+a[3]*b[1], # 1 239 | a[0]*b[2]+a[2]*b[3], # 2 240 | a[1]*b[2]+a[3]*b[3], # 3 241 | a[0]*b[4]+a[2]*b[5]+a[4], # 4 242 | a[1]*b[4]+a[3]*b[5]+a[5] # 5 243 | ] 244 | return retval 245 | 246 | 247 | # svg files can have multiple levels of transformation. 248 | # find and combine them. 249 | def combine_path_transforms(curtrans, curnode, parent_map): 250 | if ('transform' in curnode.attrib): 251 | trans = None 252 | mo = re.search("matrix\((.*)\)", curnode.attrib['transform']) 253 | if (mo): 254 | trans = [float(x) for x in mo.group(1).split(',')] 255 | 256 | mo = re.search("translate\((.*)\)", curnode.attrib['transform']) 257 | # quoting again from https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform 258 | # translate( []) 259 | # This transform definition specifies a translation by x and y. 260 | # This is equivalent to matrix(1 0 0 1 x y). 261 | # If y is not provided, it is assumed to be zero. 262 | if (mo): 263 | trans = (1, 0, 0, 1) + tuple([float(x) for x in mo.group(1).split(',')]) 264 | if len(trans) == 5: 265 | trans = trans + (0) 266 | 267 | if not trans: 268 | raise ValueError('wasnt able to match transform') 269 | 270 | if (curtrans): 271 | # need to multiply trans 272 | print("need to multiple {} and {}".format(trans, curtrans)) 273 | curtrans = multiply_transforms(trans, curtrans) 274 | else: 275 | print("have trans {}".format(trans)) 276 | curtrans = trans 277 | 278 | 279 | if (curnode not in parent_map): 280 | return curtrans 281 | 282 | return combine_path_transforms(curtrans, parent_map[curnode], parent_map) 283 | 284 | def get_mm_from_dimension(val): 285 | mo = re.match("([0-9\.\-]+)(mm|cm|m|in|ft)", val) 286 | if (not mo): 287 | raise ValueError("expecting number with mm,cm,m,in, or ft dimension, got '{}'".format(val)) 288 | 289 | num = float(mo.group(1)) 290 | dim = mo.group(2) 291 | 292 | # number to multiply by to get mm 293 | multiplier = { 294 | "mm": 1.0, 295 | "cm": 10.0, 296 | "m": 1000.0, 297 | "in": 25.4, 298 | "ft": 25.4*12 299 | } 300 | if (dim not in multiplier): 301 | raise ValueError("this shouldn't happen. expecing a dimension of mm,cm,m,in or ft. Got {}".format(dim)) 302 | 303 | return num*multiplier[dim] 304 | 305 | 306 | 307 | 308 | 309 | 310 | def parse_svg_path(filepath): 311 | tree = ET.parse(filepath) 312 | root = tree.getroot() 313 | 314 | # width="100mm" 315 | # height="100mm" 316 | # viewBox="0 0 354.33071 354.33071" 317 | width = get_mm_from_dimension(root.attrib['width']) 318 | height = get_mm_from_dimension(root.attrib['height']) 319 | box = [float(x) for x in root.attrib['viewBox'].split(" ")] 320 | 321 | # throughout path parsing, various transformations need to be applied 322 | # these are embedded in the path as well as parent groups. 323 | # the last transformation is into the target coordinate system, 324 | # based a projection of viewbox into 0->width and 0->height 325 | # xfinal = (x-xl_viewbox)/(xh_viewbox-xl_viewbox)*width 326 | # so we have a stretch factor of 1/(xh-xl)*width and an offset of xl*width 327 | 328 | # as with the rest of the transformations, I reference here: 329 | # https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform 330 | # this page is also helpful: 331 | # https://en.wikipedia.org/wiki/Transformation_matrix 332 | (xl, yl, xh, yh) = tuple(box) 333 | 334 | # a c e 1.0/(xh-xl)*width 0 xl*width 335 | # b d f 0 1.0/(yh-yl)*height yl*height 336 | # 0 0 1 337 | coordtrans = (1.0/(xh-xl)*width, 0, 338 | 0, 1.0/(yh-yl)*height, 339 | xl*width, yl*height) 340 | print("coortrans is {}".format(coordtrans)) 341 | # from here: https://stackoverflow.com/a/20132342/23630 342 | parent_map = {c:p for p in tree.iter() for c in p} 343 | 344 | retval = [] 345 | for path in root.iter('{http://www.w3.org/2000/svg}path'): 346 | points = path.attrib['d'] 347 | 348 | # here I pass in None as the initial transform. 349 | # this is because combine, combines on the way up. 350 | # coordtrans is the top most transform. 351 | # I could also have combine do the matrix stuff on the way 352 | # down, but it was already written when I realized I needed 353 | # coordtrans 354 | curtrans = combine_path_transforms(None, path, parent_map) 355 | if (curtrans): 356 | curtrans = multiply_transforms(coordtrans, curtrans) 357 | else: 358 | curtrans = coordtrans 359 | print("using transform {}".format(curtrans)) 360 | p = SVGPath(points, curtrans) 361 | retval.append(p) 362 | return retval 363 | 364 | def path_bbox(path): 365 | 366 | xl = min([min([pt[0] for pt in poly]) for poly in path.polys]) 367 | xh = max([max([pt[0] for pt in poly]) for poly in path.polys]) 368 | yl = min([min([pt[1] for pt in poly]) for poly in path.polys]) 369 | yh = max([max([pt[1] for pt in poly]) for poly in path.polys]) 370 | 371 | 372 | return (xl,yl,xh,yh) 373 | 374 | def poly_is_hole(poly): 375 | # to determine if a poly is a hole or outer boundary i check for 376 | # clockwise or counter-clockwise. 377 | # As suggested here: 378 | # https://stackoverflow.com/a/1165943/23630 379 | # I take the area under the curve, and if it's positive or negative 380 | # I'll know bounds or hole. 381 | lastpt = poly[-1] 382 | area = 0.0 383 | for pt in poly: 384 | # the area under a line is (actually twice the area, but we just 385 | # want the sign 386 | area = area + (pt[0]-lastpt[0])*(pt[1]+lastpt[1]) 387 | lastpt = pt 388 | return (area<0.0) 389 | 390 | # paths= parse_svg_path('/home/mmccoo/kicad/kicad_mmccoo/svg2border/drawing.svg') 391 | # for path in paths: 392 | # print("path") 393 | # for poly in path.polys: 394 | # print(" points {}".format(poly)) 395 | -------------------------------------------------------------------------------- /svg2border/svg2border.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import pcbnew 4 | import wx 5 | import pdb 6 | 7 | import inspect 8 | 9 | import sys, os.path 10 | import parse_svg_path 11 | 12 | from ..simpledialog import DialogUtils 13 | 14 | # I know, this code should really be merged into the dxf stuff. The same command could read 15 | # svg and dxf and then use the callback actions classes. 16 | # The two features were written at two different times,... whatever, 17 | # Just don't feel like it at the moment. ;) 18 | 19 | 20 | class SVG2ZoneDialog(DialogUtils.BaseDialog): 21 | def __init__(self): 22 | super(SVG2ZoneDialog, self).__init__("SVG Dialog") 23 | 24 | homedir = os.path.expanduser("~") 25 | self.file_picker = DialogUtils.FilePicker(self, homedir, 26 | wildcard="SVG files (.svg)|*.svg", 27 | configname="SVG2ZoneDialog") 28 | self.AddLabeled(item=self.file_picker, label="SVG file", 29 | proportion=0, flag=wx.ALL, border=2) 30 | 31 | self.basic_layer = DialogUtils.BasicLayerPicker(self, layers=['F.Cu', 'B.Cu']) 32 | self.AddLabeled(item=self.basic_layer, label="Target layer", border=2) 33 | 34 | self.net = DialogUtils.NetPicker(self) 35 | self.AddLabeled(item=self.net, 36 | label="Target Net", 37 | proportion=1, 38 | flag=wx.EXPAND|wx.ALL, 39 | border=2) 40 | 41 | 42 | 43 | # make the dialog a little taller than minimum to give the layer list a bit more space. 44 | # self.IncSize(height=5) 45 | 46 | 47 | class SVG2ZonePlugin(pcbnew.ActionPlugin): 48 | def defaults(self): 49 | self.name = "Convert an SVG to a zone...)" 50 | self.category = "A descriptive category name" 51 | self.description = "This plugin reads a svg file and converts it to a graphic" 52 | 53 | def Run(self): 54 | dlg = SVG2ZoneDialog() 55 | res = dlg.ShowModal() 56 | 57 | if res == wx.ID_OK: 58 | print("ok") 59 | 60 | if (dlg.net.value == None): 61 | warndlg = wx.MessageDialog(self, "no net was selected", "Error", wx.OK | wx.ICON_WARNING) 62 | warndlg.ShowModal() 63 | warndlg.Destroy() 64 | return 65 | 66 | # do it. 67 | SVG2Zone(dlg.file_picker.value, 68 | pcbnew.GetBoard(), 69 | dlg.basic_layer.valueint, 70 | dlg.net.GetValuePtr()) 71 | else: 72 | print("cancel") 73 | 74 | 75 | class SVG2GraphicDialog(DialogUtils.BaseDialog): 76 | def __init__(self): 77 | super(SVG2GraphicDialog, self).__init__("SVG Dialog") 78 | 79 | homedir = os.path.expanduser("~") 80 | self.file_picker = DialogUtils.FilePicker(self, homedir, 81 | wildcard="SVG files (.svg)|*.svg", 82 | configname="SVG2GraphicDialog") 83 | self.AddLabeled(item=self.file_picker, label="SVG file", 84 | proportion=0, flag=wx.ALL, border=2) 85 | 86 | self.basic_layer = DialogUtils.BasicLayerPicker(self, layers=['F.SilkS', 'Eco1.User','Dwgs.User','Edge.Cuts', 87 | 'B.Silks', 'Eco2.User','Cmts.User']) 88 | self.AddLabeled(item=self.basic_layer, label="Target layer", border=2) 89 | 90 | 91 | # make the dialog a little taller than minimum to give the layer list a bit more space. 92 | # self.IncSize(height=5) 93 | 94 | 95 | class SVG2GraphicPlugin(pcbnew.ActionPlugin): 96 | def defaults(self): 97 | self.name = "Convert an SVG to a graphic (Silk, EdgeCuts,...)" 98 | self.category = "A descriptive category name" 99 | self.description = "This plugin reads a svg file and converts it to a graphic" 100 | 101 | def Run(self): 102 | dlg = SVG2GraphicDialog() 103 | res = dlg.ShowModal() 104 | 105 | if res == wx.ID_OK: 106 | print("ok") 107 | 108 | # do it. 109 | SVG2Graphic(dlg.file_picker.value, 110 | pcbnew.GetBoard(), 111 | dlg.basic_layer.valueint) 112 | else: 113 | print("cancel") 114 | 115 | 116 | SVG2ZonePlugin().register() 117 | SVG2GraphicPlugin().register() 118 | 119 | 120 | def SVG2Zone(filename, board, layer, net): 121 | 122 | # the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm) 123 | # the coordinate 121550000 corresponds to 121.550000 124 | SCALE = 1000000.0 125 | 126 | 127 | # here I load from drawing.svg in the current directory. You'll want to change that path. 128 | paths = parse_svg_path.parse_svg_path(filename) 129 | if not paths: 130 | raise ValueError('wasnt able to read any paths from file') 131 | 132 | 133 | # things are a little tricky below, because the first boundary has its first 134 | # point passed into the creation of the new area. subsequent bounds are not 135 | # done that way. 136 | zone_container = None 137 | shape_poly_set = None 138 | 139 | for path in paths: 140 | for shape in path.group_by_bound_and_holes(): 141 | shapeid = None 142 | if not shape_poly_set: 143 | # the call to GetNet() gets the netcode, an integer. 144 | zone_container = board.InsertArea(net.GetNet(), 0, layer, 145 | int(shape.bound[0][0]*SCALE), 146 | int(shape.bound[0][1]*SCALE), 147 | pcbnew.CPolyLine.DIAGONAL_EDGE) 148 | shape_poly_set = zone_container.Outline() 149 | shapeid = 0 150 | else: 151 | shapeid = shape_poly_set.NewOutline() 152 | shape_poly_set.Append(int(shape.bound[0][0]*SCALE), 153 | int(shape.bound[0][1]*SCALE), 154 | shapeid) 155 | 156 | for pt in shape.bound[1:]: 157 | shape_poly_set.Append(int(pt[0]*SCALE), int(pt[1]*SCALE)) 158 | 159 | for hole in shape.holes: 160 | hi = shape_poly_set.NewHole() 161 | # -1 to the third arg maintains the default behavior of 162 | # using the last outline. 163 | for pt in hole: 164 | shape_poly_set.Append(int(pt[0]*SCALE), int(pt[1]*SCALE), -1, hi) 165 | 166 | zone_container.Hatch() 167 | 168 | 169 | def make_line(board, start, end, layer): 170 | 171 | start = pcbnew.wxPoint(pcbnew.Millimeter2iu(start[0]), pcbnew.Millimeter2iu(start[1])) 172 | end = pcbnew.wxPoint(pcbnew.Millimeter2iu(end[0]), pcbnew.Millimeter2iu(end[1])) 173 | if (start == end): 174 | return 175 | seg = pcbnew.DRAWSEGMENT(board) 176 | seg.SetLayer(layer) 177 | seg.SetShape(pcbnew.S_SEGMENT) 178 | seg.SetStart(start) 179 | seg.SetEnd(end) 180 | board.Add(seg) 181 | 182 | 183 | def SVG2Graphic(filename, board, layer): 184 | # the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm) 185 | # the coordinate 121550000 corresponds to 121.550000 186 | SCALE = 1000000.0 187 | 188 | 189 | # here I load from drawing.svg in the current directory. You'll want to change that path. 190 | paths = parse_svg_path.parse_svg_path(filename) 191 | if not paths: 192 | raise ValueError('wasnt able to read any paths from file') 193 | 194 | for path in paths: 195 | for shape in path.group_by_bound_and_holes(): 196 | 197 | lastPt = shape.bound[0] 198 | for pt in shape.bound[1:]: 199 | # I'm getting repeated points from the svg. haven't investigated why. 200 | if (pt == lastPt): 201 | continue 202 | make_line(board, lastPt, pt, layer) 203 | lastPt = pt 204 | 205 | 206 | for hole in shape.holes: 207 | lastPt = hole[0] 208 | for pt in hole[1:]: 209 | if (pt == lastPt): 210 | continue; 211 | make_line(board, lastPt, pt, layer) 212 | lastPt = pt 213 | 214 | 215 | 216 | # this stuff is done for you by the plugin mechanicm. 217 | # # In the future, this build connectivity call will not be neccessary. 218 | # # I have submitted a patch to include this in the code for Refresh. 219 | # # You'll know you needed it if pcbnew crashes without it. 220 | # board.BuildConnectivity() 221 | 222 | # pcbnew.Refresh() 223 | print("done") 224 | -------------------------------------------------------------------------------- /svg2border/test_hole.py: -------------------------------------------------------------------------------- 1 | import pcbnew 2 | 3 | board = pcbnew.GetBoard() 4 | nets = board.GetNetsByName() 5 | 6 | # find a power net to add the zone to. 7 | powernet = None 8 | for name in ["+12V", "+5V", "GND"]: 9 | if (nets.has_key(name)): 10 | powernet = nets[name] 11 | 12 | # generate a name->layer table so we can lookup layer numbers by name. 13 | layertable = {} 14 | 15 | # if you get an error saying that PCB_LAYER_ID_COUNT doesn't exist, then 16 | # it's probably because you're on an older version of pcbnew. 17 | # the interface has changed a little (progress) it used to be called LAYER_ID_COUNT. 18 | # now it's called PCB_LAYER_ID_COUNT 19 | if hasattr(pcbnew, "LAYER_ID_COUNT"): 20 | pcbnew.PCB_LAYER_ID_COUNT = pcbnew.LAYER_ID_COUNT 21 | 22 | 23 | numlayers = pcbnew.PCB_LAYER_ID_COUNT 24 | for i in range(numlayers): 25 | layertable[board.GetLayerName(i)] = i 26 | #print("{} {}".format(i, board.GetLayerName(i))) 27 | 28 | # the internal coorinate space of pcbnew is 10E-6 mm. (a millionth of a mm) 29 | # the coordinate 121550000 corresponds to 121.550000 30 | SCALE = 1000000.0 31 | 32 | powerlayer = layertable["B.Cu"] 33 | 34 | # class_zone.h 35 | zone_container = board.InsertArea(powernet.GetNet(), 0, powerlayer, 36 | int(10*SCALE), int(10*SCALE), 37 | pcbnew.CPolyLine.DIAGONAL_EDGE) 38 | 39 | # shape_poly_set.h 40 | # this was has been initialized with the point passed to InsertArea 41 | shape_poly_set = zone_container.Outline() 42 | 43 | # append actually takes 4 args. x,y, outline, hole 44 | # outline and hole can be omitted. 45 | # if outline is omitted, -1 is passed and the last main outline is used. 46 | # if hole is omitted, the point is added to the main outline. 47 | shape_poly_set.Append(int(10*SCALE), int(20*SCALE)) 48 | shape_poly_set.Append(int(20*SCALE), int(20*SCALE)) 49 | shape_poly_set.Append(int(20*SCALE), int(10*SCALE)) 50 | 51 | hole = shape_poly_set.NewHole() 52 | # -1 to the third arg maintains the default behavior of using the last 53 | # outline. 54 | shape_poly_set.Append(int(12*SCALE), int(12*SCALE), -1, hole) 55 | shape_poly_set.Append(int(12*SCALE), int(16*SCALE), -1, hole) 56 | shape_poly_set.Append(int(16*SCALE), int(16*SCALE), -1, hole) 57 | shape_poly_set.Append(int(16*SCALE), int(12*SCALE), -1, hole) 58 | 59 | # don't know why this is necessary. When calling InsertArea above, DIAGONAL_EDGE was passed 60 | # If you save/restore, the zone will come back hatched. 61 | # before then, the zone boundary will just be a line. 62 | # Omit this if you are using pcbnew.CPolyLine.NO_HATCH 63 | # if you don't use hatch, you won't see any hole outlines 64 | zone_container.Hatch() 65 | 66 | 67 | # In the future, this build connectivity call will not be neccessary. 68 | # I have submitted a patch to include this in the code for Refresh. 69 | # You'll know you needed it if pcbnew crashes without it. 70 | board.BuildConnectivity() 71 | 72 | pcbnew.Refresh() 73 | print("done") 74 | -------------------------------------------------------------------------------- /svg2border/test_parser.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | import sys, os.path 4 | oldpath = sys.path 5 | # inspect.stack()[0][1] is the full path to the current file. 6 | sys.path.insert(0, os.path.dirname(inspect.stack()[0][1])) 7 | import parse_svg_path 8 | sys.path = oldpath 9 | 10 | 11 | paths = parse_svg_path.parse_svg_path('/home/mmccoo/kicad/kicad_mmccoo/svg2border/drawing.svg') 12 | 13 | for path in paths: 14 | print("path {}".format(parse_svg_path.path_bbox(path))) 15 | #for poly in path.polys: 16 | #print(" points {}".format(poly)) 17 | #print(" is hole {}".format(parse_svg_path.poly_is_hole(poly))) 18 | # print(" points 18{}".format(poly)) 19 | for shape in path.group_by_bound_and_holes(): 20 | print("bounds: {}".format(shape.bound)) 21 | print("with holes:") 22 | for hole in shape.holes: 23 | print(" hole: {}".format(hole)) 24 | 25 | -------------------------------------------------------------------------------- /toggle_visibility/__init__.py: -------------------------------------------------------------------------------- 1 | import pcbnew 2 | from . import toggle_visibility 3 | 4 | class ToggleVisibilityPlugin(pcbnew.ActionPlugin): 5 | def defaults(self): 6 | self.name = "Toggle visibility of value/reference (of selected modules)" 7 | self.category = "A descriptive category name" 8 | self.description = "This plugin toggles the visibility of any selected module values/references" 9 | 10 | def Run(self): 11 | # The entry function of the plugin that is executed on user action 12 | toggle_visibility.ToggleVisibility() 13 | 14 | 15 | ToggleVisibilityPlugin().register() # Instantiate and register to Pcbnew 16 | 17 | print("done adding toggle") 18 | -------------------------------------------------------------------------------- /toggle_visibility/toggle_visibility.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import pcbnew 4 | 5 | def ToggleVisibility(): 6 | b = pcbnew.GetBoard() 7 | 8 | for mod in b.GetModules(): 9 | r = mod.Reference() 10 | if (r.IsSelected()): 11 | r.SetVisible(not r.IsVisible()) 12 | 13 | v = mod.Value() 14 | if (v.IsSelected()): 15 | v.SetVisible(not v.IsVisible()) 16 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import util_plugins 3 | -------------------------------------------------------------------------------- /utils/delaunay.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | import pcbnew 6 | import numpy as np 7 | import numpy 8 | from scipy.spatial import Delaunay 9 | #import matplotlib.pyplot as plt 10 | from scipy.sparse import csr_matrix 11 | from scipy.sparse.csgraph import minimum_spanning_tree 12 | from sets import Set 13 | import pdb 14 | 15 | 16 | if (0): 17 | pts = [] 18 | for mod in board.GetModules(): 19 | for pad in mod.Pads(): 20 | pts.append(tuple(pad.GetCenter())) 21 | 22 | pts = np.array(pts) 23 | 24 | tri = Delaunay(pts) 25 | 26 | if (0): 27 | plt.triplot(pts[:,0], pts[:,1], tri.simplices.copy()) 28 | plt.plot(pts[:,0], pts[:,1], 'o') 29 | plt.show() 30 | 31 | 32 | 33 | def draw_seg(board, p1, p2, layer): 34 | seg = pcbnew.DRAWSEGMENT(board) 35 | seg.SetShape(pcbnew.S_SEGMENT) 36 | seg.SetLayer(layer) 37 | 38 | seg.SetStart(pcbnew.wxPoint(*p1)) 39 | seg.SetEnd(pcbnew.wxPoint(*p2)) 40 | board.Add(seg) 41 | 42 | def draw_triangulation(board, layer, pts): 43 | tri = Delaunay(np.array(pts)) 44 | for simp in tri.simplices: 45 | (a,b,c) = simp 46 | draw_seg(board, pts[a], pts[b], layer) 47 | draw_seg(board, pts[b], pts[c], layer) 48 | draw_seg(board, pts[c], pts[a], layer) 49 | 50 | 51 | 52 | 53 | def GenMSTRoutes(nets, mods, layername): 54 | board = pcbnew.GetBoard() 55 | 56 | # force that nets and mods are sets. 57 | nets = Set(nets) 58 | mods = Set(mods) 59 | 60 | # generate a name->layer table so we can lookup layer numbers by name. 61 | layertable = {} 62 | numlayers = pcbnew.PCB_LAYER_ID_COUNT 63 | for i in range(numlayers): 64 | layertable[pcbnew.GetBoard().GetLayerName(i)] = i 65 | 66 | layer = layertable[layername] 67 | 68 | netpts = {} 69 | for mod in board.GetModules(): 70 | if (mod.GetReference() not in mods): 71 | continue 72 | 73 | for pad in mod.Pads(): 74 | if (pad.GetLayerName() != layername): 75 | continue 76 | netname = pad.GetNet().GetNetname() 77 | if (netname not in nets): 78 | continue 79 | 80 | if (netname not in netpts): 81 | netpts[netname] = [] 82 | netpts[netname].append(tuple(pad.GetCenter())) 83 | 84 | for via in board.GetTracks(): 85 | if not pcbnew.VIA.ClassOf(via): 86 | continue 87 | if (via.BottomLayer() != layer) and (via.TopLayer() != layer): 88 | continue 89 | netname = via.GetNet().GetNetname() 90 | if (netname not in nets): 91 | continue 92 | 93 | if (netname not in netpts): 94 | netpts[netname] = [] 95 | netpts[netname].append(tuple(via.GetPosition())) 96 | 97 | 98 | nettable = board.GetNetsByName() 99 | for netname in netpts: 100 | if (netname not in nets): 101 | continue 102 | 103 | pts = netpts[netname] 104 | matrix = np.zeros(shape=[len(pts),len(pts)]) 105 | 106 | tri = Delaunay(np.array(pts)) 107 | for simp in tri.simplices: 108 | (a,b,c) = simp 109 | matrix[a][b] = numpy.hypot(*numpy.subtract(pts[a], pts[b])) 110 | matrix[b][c] = numpy.hypot(*numpy.subtract(pts[b], pts[c])) 111 | matrix[c][a] = numpy.hypot(*numpy.subtract(pts[c], pts[a])) 112 | 113 | X = csr_matrix(matrix) 114 | Tcsr = minimum_spanning_tree(X) 115 | 116 | net = nettable[netname] 117 | nc = net.GetNetClass() 118 | #print("for net {}".format(net.GetNetname())) 119 | 120 | # info about iterating the results: 121 | # https://stackoverflow.com/a/4319087/23630 122 | rows,cols = Tcsr.nonzero() 123 | for row,col in zip(rows,cols): 124 | #print(" {} - {}".format(pts[row], pts[col])) 125 | newtrack = pcbnew.TRACK(board) 126 | # need to add before SetNet will work, so just doing it first 127 | board.Add(newtrack) 128 | newtrack.SetNet(net) 129 | newtrack.SetStart(pcbnew.wxPoint(*pts[row])) 130 | newtrack.SetEnd(pcbnew.wxPoint(*pts[col])) 131 | newtrack.SetWidth(nc.GetTrackWidth()) 132 | newtrack.SetLayer(layer) 133 | 134 | 135 | 136 | 137 | #pcbnew.Refresh() 138 | -------------------------------------------------------------------------------- /utils/groundvias.py: -------------------------------------------------------------------------------- 1 | import pcbnew 2 | from sets import Set 3 | 4 | 5 | def GroundVias(nets, modules): 6 | 7 | board = pcbnew.GetBoard() 8 | # this blog argues what I'm doing here it bad: 9 | # http://www.johngineer.com/blog/?p=1319 10 | # generate a name->layer table so we can lookup layer numbers by name. 11 | layertable = {} 12 | numlayers = pcbnew.PCB_LAYER_ID_COUNT 13 | for i in range(numlayers): 14 | layertable[pcbnew.GetBoard().GetLayerName(i)] = i 15 | 16 | modules = Set(modules) 17 | 18 | nettable = board.GetNetsByName() 19 | 20 | netcodes = Set() 21 | for name in nets: 22 | if (name in nettable): 23 | netcodes.add(nettable[name].GetNet()) 24 | 25 | toplayer = layertable['F.Cu'] 26 | bottomlayer = layertable['B.Cu'] 27 | 28 | for mod in board.GetModules(): 29 | if (mod.GetReference() not in modules): 30 | continue 31 | 32 | for pad in mod.Pads(): 33 | netcode = pad.GetNetCode() 34 | if (netcode not in netcodes): 35 | continue 36 | 37 | newvia = pcbnew.VIA(board) 38 | # need to add before SetNet will work, so just doing it first 39 | board.Add(newvia) 40 | 41 | net = pad.GetNet() 42 | newvia.SetNet(net) 43 | nc = net.GetNetClass() 44 | newvia.SetWidth(nc.GetViaDiameter()) 45 | newvia.SetPosition(pad.GetCenter()) 46 | newvia.SetLayerPair(toplayer, bottomlayer) 47 | newvia.SetViaType(pcbnew.VIA_THROUGH) 48 | 49 | #pcbnew.Refresh() 50 | -------------------------------------------------------------------------------- /utils/util_plugins.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import pcbnew 4 | import wx 5 | from ..simpledialog import DialogUtils 6 | import groundvias 7 | import delaunay 8 | import via_fill 9 | 10 | 11 | class GroundViasDialog(DialogUtils.BaseDialog): 12 | def __init__(self): 13 | super(GroundViasDialog, self).__init__("Ground vias dialog") 14 | 15 | 16 | self.nets = DialogUtils.NetPicker(self, singleton=False) 17 | self.AddLabeled(item=self.nets, 18 | label="Target Net", 19 | proportion=1, 20 | flag=wx.EXPAND|wx.ALL, 21 | border=2) 22 | 23 | self.mods = DialogUtils.ModulePicker(self, singleton=False) 24 | self.AddLabeled(item=self.mods, 25 | label="all mods", 26 | proportion=1, 27 | flag=wx.EXPAND|wx.ALL, 28 | border=2) 29 | 30 | 31 | # make the dialog a little taller than minimum to give the layer and net 32 | # lists a bit more space. 33 | self.IncSize(width=50, height=10) 34 | 35 | 36 | class GroundViasPlugin(pcbnew.ActionPlugin): 37 | def defaults(self): 38 | self.name = "Drop a via on module pads of net" 39 | self.category = "A descriptive category name" 40 | self.description = "This plugin finds all pads on selected nets and drops a via" 41 | 42 | def Run(self): 43 | dlg = GroundViasDialog() 44 | res = dlg.ShowModal() 45 | 46 | groundvias.GroundVias(dlg.nets.value, dlg.mods.value) 47 | 48 | GroundViasPlugin().register() 49 | 50 | 51 | class MSTRoutesDialog(DialogUtils.BaseDialog): 52 | def __init__(self): 53 | super(MSTRoutesDialog, self).__init__("MST Routes dialog") 54 | 55 | self.basic_layer = DialogUtils.BasicLayerPicker(self, layers=['F.Cu', 'B.Cu']) 56 | self.AddLabeled(item=self.basic_layer, label="target layer", border=2) 57 | 58 | self.nets = DialogUtils.NetPicker(self, singleton=False) 59 | self.AddLabeled(item=self.nets, 60 | label="Target Nets", 61 | proportion=1, 62 | flag=wx.EXPAND|wx.ALL, 63 | border=2) 64 | 65 | self.mods = DialogUtils.ModulePicker(self, singleton=False) 66 | self.AddLabeled(item=self.mods, 67 | label="all mods", 68 | proportion=1, 69 | flag=wx.EXPAND|wx.ALL, 70 | border=2) 71 | 72 | # make the dialog a little taller than minimum to give the layer and net 73 | # lists a bit more space. 74 | self.IncSize(width=50, height=10) 75 | 76 | class MSTRoutesPlugin(pcbnew.ActionPlugin): 77 | def defaults(self): 78 | self.name = "Generate Minimum Spanning Tree route" 79 | self.category = "A descriptive category name" 80 | self.description = "This plugin computes an MST for selected nets/modules and generates a route from that" 81 | 82 | def Run(self): 83 | dlg = MSTRoutesDialog() 84 | res = dlg.ShowModal() 85 | 86 | delaunay.GenMSTRoutes(dlg.nets.value, dlg.mods.value, dlg.basic_layer.value) 87 | 88 | MSTRoutesPlugin().register() 89 | 90 | 91 | class ViaFillDialog(DialogUtils.BaseDialog): 92 | def __init__(self): 93 | super(ViaFillDialog, self).__init__("Via Fill dialog") 94 | 95 | self.nets = DialogUtils.NetPicker(self, singleton=False) 96 | self.AddLabeled(item=self.nets, 97 | label="Target Nets", 98 | proportion=1, 99 | flag=wx.EXPAND|wx.ALL, 100 | border=2) 101 | 102 | class ViaFillPlugin(pcbnew.ActionPlugin): 103 | def defaults(self): 104 | self.name = "Fill vias" 105 | self.category = "A descriptive category name" 106 | self.description = "This plugin tries to place many vias between zones of a net." 107 | 108 | def Run(self): 109 | dlg = ViaFillDialog() 110 | res = dlg.ShowModal() 111 | 112 | via_fill.ViaFill(dlg.nets.value) 113 | 114 | 115 | ViaFillPlugin().register() 116 | -------------------------------------------------------------------------------- /utils/via_fill.py: -------------------------------------------------------------------------------- 1 | 2 | import pcbnew 3 | 4 | import pdb 5 | 6 | import numpy as np 7 | import math 8 | from sets import Set 9 | 10 | showplot = False 11 | if showplot: 12 | import matplotlib.pyplot as plt 13 | fig, ax = plt.subplots() 14 | 15 | from shapely.ops import polygonize 16 | from shapely.geometry import Polygon 17 | from shapely.geometry import MultiPolygon 18 | from shapely.geometry import CAP_STYLE, JOIN_STYLE 19 | from shapely.ops import cascaded_union 20 | from shapely.geometry import box 21 | from shapely.geometry import LineString 22 | from shapely.geometry import Point 23 | 24 | # this shouldn't be a global. it will be populated by viafill() 25 | layertable = {} 26 | 27 | def iterable(a): 28 | try: 29 | (x for x in a) 30 | return True 31 | except TypeError: 32 | return False 33 | 34 | 35 | 36 | def draw_poly(board, polys, layer): 37 | # sometimes shapely returns a poly sometimes a multi. 38 | if not iterable(polys): 39 | polys = [polys] 40 | 41 | for poly in polys: 42 | if (not getattr(poly, "exterior", None)): 43 | print("got line? " + str(poly)) 44 | continue 45 | 46 | 47 | seg = pcbnew.DRAWSEGMENT(board) 48 | seg.SetLayer(layer) 49 | seg.SetShape(pcbnew.S_SEGMENT) 50 | board.Add(seg) 51 | 52 | seg.SetShape(pcbnew.S_POLYGON) 53 | 54 | sps = seg.GetPolyShape() 55 | 56 | o = sps.NewOutline() 57 | 58 | # shapely polygons start and end with the same coord 59 | # so skip the first 60 | print("ext {}".format(len(list(poly.exterior.coords)[1:]))) 61 | for pt in list(poly.exterior.coords)[1:]: 62 | sps.Append(int(pt[0]), int(pt[1]), o) 63 | 64 | for hole in poly.interiors: 65 | h = sps.NewHole() 66 | print(" hole {}".format(len(list(hole.coords)[1:]))) 67 | for pt in list(hole.coords)[1:]: 68 | sps.Append(int(pt[0]), int(pt[1]), o, h) 69 | 70 | 71 | def coordsFromPolySet(ps): 72 | str = ps.Format() 73 | lines = str.split('\n') 74 | numpts = int(lines[2]) 75 | pts = [[int(n) for n in x.split(" ")] for x in lines[3:-2]] # -1 because of the extra two \n 76 | return pts 77 | 78 | def plot_poly(polys): 79 | for poly in polys: 80 | if (not getattr(poly, "exterior", None)): 81 | print("got line?") 82 | continue 83 | dpts = list(poly.exterior.coords) 84 | x,y = zip(*dpts) 85 | if showplot: 86 | plt.plot(x,y) 87 | 88 | for hole in poly.interiors: 89 | hpts = list(hole.coords) 90 | x,y = zip(*hpts) 91 | if showplot: 92 | plt.plot(x,y) 93 | 94 | def create_via(board, net, pt): 95 | newvia = pcbnew.VIA(board) 96 | # need to add before SetNet will work, so just doing it first 97 | board.Add(newvia) 98 | toplayer = layertable['F.Cu'] 99 | bottomlayer = layertable['B.Cu'] 100 | 101 | newvia.SetNet(net) 102 | nc = net.GetNetClass() 103 | newvia.SetWidth(nc.GetViaDiameter()) 104 | newvia.SetPosition(pcbnew.wxPoint(*pt)) 105 | newvia.SetLayerPair(toplayer, bottomlayer) 106 | newvia.SetViaType(pcbnew.VIA_THROUGH) 107 | 108 | # I don't want to update the geometries everytime I add something. 109 | added = [] 110 | def add_via_at_pt(board, net, pt, viadiameter): 111 | x,y = pt 112 | for xother,yother in added: 113 | if np.hypot(x-xother, y-yother) < viadiameter: 114 | #print("{},{} and {},{} have distance of {}".format(x,y,xother,yother,np.hypot(x-xother, y-yother))) 115 | return 116 | if showplot: 117 | ax.add_artist(plt.Circle((x,y), viadiameter/2)) 118 | create_via(board, net, (x,y)) 119 | added.append((x,y)) 120 | 121 | # this fun takes a bunch of lines and converts to a boundary with holes 122 | # seems like there'd be a standard way to do this. I haven't found it. 123 | def LinesToPolyHoles(lines): 124 | # get all of the polys, both outer and holes 125 | # if there are holes, you'll get them double: 126 | # once as themselves and again as holes in a boundary 127 | polys = list(polygonize(lines)) 128 | 129 | # merge to get just the outer 130 | bounds = cascaded_union(polys) 131 | 132 | if not iterable(bounds): 133 | bounds = [bounds] 134 | 135 | retval = [] 136 | for bound in bounds: 137 | for poly in polys: 138 | if Polygon(poly.exterior).almost_equals(bound): 139 | retval.append(poly) 140 | 141 | return retval 142 | 143 | 144 | def ViaFill(nets): 145 | 146 | nets = Set(nets) 147 | 148 | board = pcbnew.GetBoard() 149 | 150 | global layertable 151 | layertable = {} 152 | numlayers = pcbnew.PCB_LAYER_ID_COUNT 153 | for i in range(numlayers): 154 | layertable[pcbnew.GetBoard().GetLayerName(i)] = i 155 | 156 | #https://matplotlib.org/tutorials/introductory/usage.html#sphx-glr-tutorials-introductory-usage-py 157 | 158 | # here I get all of the obstacles. 159 | # I avoid module footprints 160 | mods = [] 161 | for mod in board.GetModules(): 162 | lines = [] 163 | for edge in mod.GraphicalItemsList(): 164 | if type(edge) != pcbnew.EDGE_MODULE: 165 | continue 166 | 167 | if edge.GetShapeStr() == 'Circle': 168 | mods.append(Point(edge.GetPosition()).buffer(edge.GetRadius())) 169 | else: 170 | lines.append(LineString([edge.GetStart(), edge.GetEnd()])) 171 | 172 | # polygonize returns a generator. the polygons needs to be extracted from that. 173 | if (len(lines)): 174 | mods.extend(polygonize(lines)) 175 | 176 | 177 | #lines.append(((x1,y1),(x2,y2))) 178 | #modpolys = MultiPolygon(polygonize(lines)).buffer(pcbnew.Millimeter2iu(.6), 179 | # cap_style=CAP_STYLE.square) 180 | 181 | #for poly in modpolys: 182 | # points = list(poly.exterior.coords) 183 | # x,y = zip(*points) 184 | # #plt.plot(x,y) 185 | 186 | tracks = [] 187 | for track in board.GetTracks(): 188 | # getTracks actually returns both vias and wires. Since 189 | # I'm trying to fill vias on a two layer board, it doesn't 190 | # matter if the object is a via or wire. 191 | # It actually could matter if it's a wire on the same net as the 192 | # fill area, but I don't care at the moment. 193 | tracks.append(LineString([track.GetStart(), track.GetEnd()]). 194 | buffer(track.GetWidth()/2+pcbnew.Millimeter2iu(.2), 195 | cap_style=CAP_STYLE.square)) 196 | 197 | bounds = [] 198 | for d in board.GetDrawings(): 199 | if d.GetLayerName() != "Edge.Cuts": 200 | continue 201 | if (d.GetShape() == pcbnew.S_CIRCLE): 202 | c = Point(d.GetPosition()).buffer(d.GetRadius()) 203 | bounds.append(c) 204 | else: 205 | bounds.append(LineString([d.GetStart(), d.GetEnd()])) 206 | 207 | 208 | for netname in nets: 209 | if (netname not in board.GetNetsByName()): 210 | print("net {} not present".format(netname)) 211 | continue 212 | net = board.GetNetsByName()[netname] 213 | 214 | tzonepolygons = [] 215 | bzonepolygons = [] 216 | for zone in board.Zones(): 217 | netcode = zone.GetNet().GetNet() 218 | # pointer comparison doesn't work. have to compare netcodes. 219 | if (zone.GetNet().GetNet() != net.GetNet()): 220 | continue 221 | shape_poly_set = zone.Outline() 222 | zonepts = coordsFromPolySet(shape_poly_set) 223 | x,y = zip(*zonepts) 224 | polygon = Polygon(zonepts) 225 | if (zone.GetLayerName() == "B.Cu"): 226 | bzonepolygons.append(polygon) 227 | else: 228 | tzonepolygons.append(polygon) 229 | 230 | netclass = net.GetNetClass() 231 | viadiameter = netclass.GetViaDiameter() 232 | 233 | obstacles = cascaded_union(mods + tracks).buffer(viadiameter/2, 234 | cap_style=CAP_STYLE.flat, 235 | join_style=JOIN_STYLE.mitre) 236 | #draw_poly(board, obstacles, layertable['Eco1.User']) 237 | 238 | 239 | # cascaded_union takes a list of polys and merges/unions them together 240 | overlaps = cascaded_union(bzonepolygons).intersection(cascaded_union(tzonepolygons)).buffer(-viadiameter/2) 241 | 242 | # unlike the other polys in this function, the boundary can have holes. 243 | boundspoly = MultiPolygon(LinesToPolyHoles(bounds)).buffer(-viadiameter/2) 244 | 245 | overlapsinbound = overlaps.intersection(cascaded_union(boundspoly)) 246 | 247 | viaspots = list(overlapsinbound.difference(obstacles)) 248 | #draw_poly(board, viaspots, layertable['Eco1.User']) 249 | 250 | 251 | # this gives list of polygons where vias can be placed. 252 | # I got some unexpected behavior from shrinking. there's a self intersecting poly 253 | # in there I think. 254 | # viaspots above used to be called diff 255 | #viaspots = diff.buffer(-viadiameter/2, join_style=JOIN_STYLE.mitre) 256 | #draw_poly(board, viaspots, layertable['Eco1.User']) 257 | 258 | # if buffer can return only one polygon that's what it will do. 259 | # otherwise it gives a list of polys 260 | 261 | 262 | for spot in viaspots: 263 | if spot.is_empty: 264 | continue 265 | coords = list(spot.exterior.coords) 266 | prevpt = coords[-2] 267 | pt = coords[-1] 268 | for nextpt in coords: 269 | angle = math.degrees(math.atan2(nextpt[1]-pt[1],nextpt[0]-pt[0])- 270 | math.atan2(pt[1]-prevpt[1],pt[0]-prevpt[0])) 271 | 272 | 273 | length=np.hypot(pt[0]-prevpt[0], pt[1]-prevpt[1]) 274 | 275 | # I don't really want to maximize vias. 276 | numvias = int(length/(viadiameter+pcbnew.Millimeter2iu(5)))+1 277 | 278 | if (numvias == 1): 279 | x = (prevpt[0]+pt[0])/2 280 | y = (prevpt[1]+pt[1])/2 281 | add_via_at_pt(board, net, (x,y), viadiameter) 282 | prevpt = pt 283 | pt = nextpt 284 | continue 285 | 286 | xincr = (pt[0]-prevpt[0])/(numvias-1) 287 | yincr = (pt[1]-prevpt[1])/(numvias-1) 288 | for i in range(numvias): 289 | x = prevpt[0] + i*xincr 290 | y = prevpt[1] + i*yincr 291 | add_via_at_pt(board, net, (x,y), viadiameter) 292 | 293 | prevpt = pt 294 | pt = nextpt 295 | 296 | 297 | #plot_poly(viaspots) 298 | 299 | if showplot: 300 | plt.show() 301 | 302 | 303 | #pcbnew.Refresh() 304 | -------------------------------------------------------------------------------- /zonebug/zonebug.py: -------------------------------------------------------------------------------- 1 | # Copyright [2017] [Miles McCoo] 2 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # this script is to reproduce a bug (I think it's a bug) in the zone addition APIs 16 | # if you don't call newoutline.CloseLastContour() after calls to 17 | # newoutline.AppendCorner, saving to file will yield a file that won't load. 18 | 19 | 20 | import pcbnew 21 | 22 | board = pcbnew.GetBoard() 23 | 24 | # generate a name->layer table so we can lookup layer numbers by name. 25 | layertable = {} 26 | numlayers = pcbnew.LAYER_ID_COUNT 27 | for i in range(numlayers): 28 | layertable[board.GetLayerName(i)] = i 29 | 30 | 31 | nets = board.GetNetsByName() 32 | randomnet = None 33 | for netpair in nets.items(): 34 | if (len(str(netpair[0])) != 0): 35 | randomnet = netpair[1] 36 | break 37 | 38 | layer = layertable["B.Cu"] 39 | 40 | 41 | newarea = board.InsertArea(net.GetNet(), 0, layer, 42 | pcbnew.FromMM(10), pcbnew.FromMM(10), 43 | pcbnew.CPolyLine.DIAGONAL_EDGE) 44 | 45 | newoutline = newarea.Outline() 46 | newoutline.AppendCorner(pcbnew.FromMM(10), pcbnew.FromMM(20)); 47 | newoutline.AppendCorner(pcbnew.FromMM(20), pcbnew.FromMM(20)); 48 | newoutline.AppendCorner(pcbnew.FromMM(20), pcbnew.FromMM(10)); 49 | 50 | 51 | # this next line shouldn't really be necessary but without it, saving to 52 | # file will yield a file that won't load. 53 | # newoutline.CloseLastContour() 54 | 55 | newoutline.Hatch() 56 | 57 | 58 | # Error loading board. 59 | # IO_ERROR: Expecting 'net, layer, tstamp, hatch, priority, connect_pads, min_thickness, fill, polygon, filled_polygon, or fill_segments' in input/source 60 | # '/bubba/electronicsDS/kicad/leddriver2/bug.kicad_pcb' 61 | # line 2947, offset 4 62 | 63 | # from dsnlexer.cpp : Expecting() line:369 64 | 65 | 66 | print("done") 67 | --------------------------------------------------------------------------------