├── .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 | 
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 | [](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 |
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 |
--------------------------------------------------------------------------------