├── .gitignore
├── LICENSE
├── README.md
├── Stretch
├── __init__.py
├── icons
│ ├── to_pcb.png
│ └── to_svg.png
└── kiplug
│ ├── __init__.py
│ ├── arc.py
│ ├── board.py
│ ├── circle.py
│ ├── colour.py
│ ├── curve.py
│ ├── kicad.py
│ ├── layers.py
│ ├── line.py
│ ├── metadata.py
│ ├── module.py
│ ├── pad.py
│ ├── parser_base.py
│ ├── pcb_writer.py
│ ├── poly.py
│ ├── segment.py
│ ├── sexpressions_writer.py
│ ├── svg_writer.py
│ ├── svgpath.py
│ ├── text.py
│ ├── via.py
│ └── zone.py
├── __init__.py
├── docs
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── inkscape-close-in.png
├── inkscape-close-out.png
├── inkscape1.png
├── kicad-close-in.png
├── kicad-close-out.png
├── kicad1.png
└── logo.png
├── pcm
├── build.py
├── metadata_template.json
└── resources
│ └── icon.png
└── tests
├── base.svg
├── complex.kicad_pcb
├── simple.kicad_pcb
└── simple.svg
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | *.svg
10 |
11 | # Distribution / packaging
12 | .Python
13 | build/
14 | develop-eggs/
15 | dist/
16 | downloads/
17 | eggs/
18 | .eggs/
19 | lib/
20 | lib64/
21 | parts/
22 | sdist/
23 | var/
24 | wheels/
25 | pip-wheel-metadata/
26 | share/python-wheels/
27 | *.egg-info/
28 | .installed.cfg
29 | *.egg
30 | MANIFEST
31 |
32 | # PyInstaller
33 | # Usually these files are written by a python script from a template
34 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
35 | *.manifest
36 | *.spec
37 |
38 | # Installer logs
39 | pip-log.txt
40 | pip-delete-this-directory.txt
41 |
42 | # Unit test / coverage reports
43 | htmlcov/
44 | .tox/
45 | .nox/
46 | .coverage
47 | .coverage.*
48 | .cache
49 | nosetests.xml
50 | coverage.xml
51 | *.cover
52 | *.py,cover
53 | .hypothesis/
54 | .pytest_cache/
55 |
56 | # Translations
57 | *.mo
58 | *.pot
59 |
60 | # Django stuff:
61 | *.log
62 | local_settings.py
63 | db.sqlite3
64 | db.sqlite3-journal
65 |
66 | # Flask stuff:
67 | instance/
68 | .webassets-cache
69 |
70 | # Scrapy stuff:
71 | .scrapy
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 |
76 | # PyBuilder
77 | target/
78 |
79 | # Jupyter Notebook
80 | .ipynb_checkpoints
81 |
82 | # IPython
83 | profile_default/
84 | ipython_config.py
85 |
86 | # pyenv
87 | .python-version
88 |
89 | # pipenv
90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
93 | # install all needed dependencies.
94 | #Pipfile.lock
95 |
96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
97 | __pypackages__/
98 |
99 | # Celery stuff
100 | celerybeat-schedule
101 | celerybeat.pid
102 |
103 | # SageMath parsed files
104 | *.sage.py
105 |
106 | # Environments
107 | .env
108 | .venv
109 | env/
110 | venv/
111 | ENV/
112 | env.bak/
113 | venv.bak/
114 | *.code-workspace
115 |
116 | # Spyder project settings
117 | .spyderproject
118 | .spyproject
119 |
120 | # Rope project settings
121 | .ropeproject
122 |
123 | # mkdocs documentation
124 | /site
125 |
126 | # mypy
127 | .mypy_cache/
128 | .dmypy.json
129 | dmypy.json
130 |
131 | # Pyre type checker
132 | .pyre/
133 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 
3 |
4 | # Stretch
5 |
6 | Allow your PCBs to _stretch_!
7 |
8 | ## The Process
9 |
10 | In KiCad:
11 |
12 | 
13 |
14 |
15 | In Inkscape:
16 |
17 |
18 | 
19 |
20 |
21 | Modify:
22 |
23 |
24 | 
25 |
26 |
27 | Back to KiCad:
28 |
29 |
30 | 
31 |
32 |
33 | ## Why?
34 |
35 | This was written to address some frustrations with other "artistic" PCB workflows.
36 | KiCad and other traditional ECAD software has poor support for curved lines, importing and processing of images/drawings, and many other features that can be expected in proper vector software.
37 |
38 |
39 | Tools intended to bridge the gap are also lacking. [PCBmodE](https://github.com/boldport/pcbmode) is the probably the best tool out there from a pure PCB art standpoint, and it is excellent, but there is no schematic tool. And with that, no ability to handle extremely complex, functional PCBs.
40 |
41 | KiCad has a tool to export to SVG. SVG-To-Shenzen is a tool to convert SVG files into KiCad files. Both of these are single-direction tools.
42 |
43 | Stretch goes both ways. Much as the ideal schematic->PCB capture workflow does not exist, the PCB layout aspect must go hand-in-hand with the art aspect.
44 |
45 | Users can start by drawing a schematic and laying out a PCB, then bring it into Inkscape to arrange a thousand LEDs into a flower arrangement, then bring it back into KiCad to lay out traces, back into Inkscape to curve the traces, back into KiCad to change their microcontroller and few pin assignments, back into Inkscape to draw out some silkscreem patterns, back into KiCad to run DRC, and so on.
46 | The workflow is intended to be seamless and painless to go back and forth.
47 |
48 | ## Installation
49 |
50 | - Copy this main Stretch folder into your KiCad plugin folder at `Tools->External Plugins...->Open Plugin Directory`
51 | - Ensure the BeautifulSoup4 library is installed (e.g. run `pip install bs4` in a system command line, see Troubleshooting below for details)
52 | - Open up a PCB and then in Pcbnew, got to `Tools->External Plugins...->Refresh Plugins`
53 | - This has been tested on KiCad 6 and later. For a KiCad 5 legacy version, check the releases.
54 |
55 | ### Troubleshooting
56 |
57 | If the buttons do not show up on the toolbar, the plugin may be installed but not loading successfully. The most common cause is a missing `bs4` library, which is required to run Stretch. Firstly, check to see if there are any error messages by opening up the Scripting Console from `Tools->Scripting Console` and in the "Shell" section at the top, click after the `>>>` prompt (you may have to scroll down) and type `from com_github_jarrettr_stretch import Stretch`. If you get the `>>>` prompt again without any other text, the plugin is loading correctly. Otherwise, there will be an error message: pay attention to the last line.
58 |
59 | If it says `No module named 'bs4'`, BeatifulSoup is not installed. If you have installed it but are still getting this error, try using `pip3` instead of `pip`, or install the library with your package manager (e.g. `apt install python3-bs4`).
60 |
61 | It's also possible to have multiple installations of Python, in which case you need to run the `pip` command from the correct installation, which can be difficult to determine. This is often the case on MacOS, where KiCad uses its own installation of Python than the system installation. To find out where the Python that KiCad is using is, go back to the `>>>` prompt above and type:
62 | ```
63 | import sys
64 | print(sys.base_exec_prefix)
65 | ```
66 | This will print out a path (e.g. `/usr` or `/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/Current`). You should be able to add `/bin/pip` (or `pip3`) to this path and run e.g. `/Applications/KiCad/KiCad.app/Contents/Frameworks/Python.framework/Versions/Current/bin/pip3 install bs4` to install BeautifulSoup in the Python installation that KiCad is using.
67 |
68 | After installing BeautifulSoup, use `Tools->External Plugins->Refresh Plugins` or restart KiCad to attempt re-loading Stretch. If all went well, you should have the up/down buttons on your toolbar.
69 |
70 | If there is an error message other than missing `bs4`, try searching with the error message, and if that is inconclusive, open an issue in this repository and copy/paste the error message here.
71 |
72 | ## Workflow
73 |
74 | - In Kicad: Open up your KiCad project, and then your PCB in Pcbnew
75 | - Hit the "UP" arrow in your plugin bar  *(Stretch up to SVG)*
76 | - In Inkscape: There is now a file in your PCB directory called "out.svg". Open it in Inkscape, or your preferred vector software
77 | - Make your modifications. Bend some lines, draw some pictures, rotate some footprints
78 | - Save your SVG
79 | - In KiCad/Pcbnew: Hit the "DOWN" arrow  *(Stretch down to PCB)*
80 | - Close Pcbnew and then re-open the PCB in the main KiCad window
81 | - Continue tweaking. Change some nets, add more components
82 | - Hit the "UP" arrow again to go right back to SVG
83 | - In Inkscape: File->Revert to quickly reload the SVG. **Alt-F** and then **V** works as well
84 | - Do it all again.
85 |
86 | In Inkscape, go to `Layer->Layers...` to display a window that will allow you to show/hide/view specific PCB layers. `Edit->XML Editor...` is also a good tool to debug why something isn't working, or see what's happening under the hood. Each object has some metadata that is associated with the KiCad PCB data, so objects without this data won't work properly. It's better to copy an existing trace/zone/via/text than to try to create one from scratch.
87 |
88 | ## Limitations
89 |
90 | Fonts will look different in your vector software than in KiCad. This includes positioning, sizing, and fonts. It will import back into the PCB editor undamaged, however.
91 |
92 | Closing the PCB window and opening it again is annoying - There is no way to programmatically reload it from disk, as of KiCad 5. Perhaps rebuilding the existing PCB from file using the API is possible, or KiCad 6 has added some other method.
93 |
94 | Metadata that doesn't need to be processed, or do not yet have processors written are still stored. They are unchanged and stored in the SVG so they can be imported properly back into the PCB.
95 |
96 | Most data is processed properly! Diffs on some mildly complicated 4-layer boards are coming back clean.
97 |
98 | This is still kind of a hack. Obviously, save and backup everything before using this, and **check your gerbers carefully** before purchasing anything. One common failure mode is that *(Stretch down to PCB)* will generate a corrupt/invalid PCB. In that case, fix the error (and report the issue), and open the backup. Clicking *(Stretch down to PCB)* will leave the backup untouched, and you can open the main file again.
99 |
100 |
101 | I am happy to accept issues, or pull requests, and example PCBs that break the software. I may be slow to fix issues by myself, so feel free to dive in yourself!
102 |
--------------------------------------------------------------------------------
/Stretch/__init__.py:
--------------------------------------------------------------------------------
1 | from .kiplug.kicad import Stretch
2 |
3 | plugin_svg = Stretch("to_svg")
4 | plugin_svg.register()
5 | plugin_pcb = Stretch("to_pcb")
6 | plugin_pcb.register()
7 |
--------------------------------------------------------------------------------
/Stretch/icons/to_pcb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/Stretch/icons/to_pcb.png
--------------------------------------------------------------------------------
/Stretch/icons/to_svg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/Stretch/icons/to_svg.png
--------------------------------------------------------------------------------
/Stretch/kiplug/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | # __all__ = [
4 | # 'What',
5 | # 'Arc',
6 | # 'Board',
7 | # 'Curve',
8 | # 'Layers',
9 | # 'Line',
10 | # 'Metadata',
11 | # 'Module',
12 | # 'Pad',
13 | # 'Poly',
14 | # 'Segment',
15 | # 'Text',
16 | # 'Via',
17 | # 'Zone'
18 | # ]
19 |
20 | # from .what import What
21 | # from .arc import Arc
22 | # from .board import Board
23 | # from .curve import Curve
24 | # from .layers import Layers
25 | # from .line import Line
26 | # from .metadata import Metadata
27 | # from .module import Module
28 | # from .pad import Pad
29 | # from .poly import Poly
30 | # from .segment import Segment
31 | # from .text import Text
32 | # from .via import Via
33 | # from .zone import Zone
--------------------------------------------------------------------------------
/Stretch/kiplug/arc.py:
--------------------------------------------------------------------------------
1 | from .colour import Colour
2 | from .svgpath import parse_path
3 | import math
4 | import cmath
5 |
6 | # https://gitlab.com/kicad/code/kicad/-/blob/14c5f744ff2edd527dec17653fd7795cb6a74299/pcbnew/plugins/kicad/pcb_parser.cpp#L4765
7 |
8 | # 0 gr_arc
9 | # 1
10 | # 0 start
11 | # 1 66.66
12 | # 2 99.99
13 | # 2
14 | # 0 mid
15 | # 1 66.66
16 | # 2 99.99
17 | # 3
18 | # 0 end
19 | # 1 66.66
20 | # 2 99.99
21 | # 4
22 | # 0 layer
23 | # 1 Edge.Cuts
24 | # 5
25 | # 0 width
26 | # 1 0.05
27 | # 6
28 | # 0 tstamp
29 | # 1 5E451B20
30 |
31 |
32 | pxToMM = 96 / 25.4
33 |
34 | class Arc(object):
35 |
36 | def __init__(self):
37 | self.start = []
38 | self.mid = []
39 | self.end = []
40 | self.width = 0
41 | self.layer = ''
42 | self.fill = ''
43 | self.tstamp = ''
44 | self.net = ''
45 | self.status = 0
46 |
47 | def From_PCB(self, input):
48 | start = []
49 | end = []
50 | centre = []
51 | tstamp = ''
52 |
53 | for item in input:
54 | if type(item) == str:
55 | continue
56 |
57 | if item[0] == 'start':
58 | self.start.append(item[1])
59 | self.start.append(item[2])
60 |
61 | if item[0] == 'mid':
62 | self.mid.append(item[1])
63 | self.mid.append(item[2])
64 |
65 | if item[0] == 'end':
66 | self.end.append(item[1])
67 | self.end.append(item[2])
68 |
69 | if item[0] == 'layer':
70 | self.layer = item[1]
71 |
72 | if item[0] == 'width':
73 | self.width = item[1]
74 |
75 | if item[0] == 'fill':
76 | self.fill = item[1]
77 |
78 | if item[0] == 'tstamp':
79 | self.tstamp = item[1]
80 |
81 | if item[0] == 'net':
82 | self.net = item[1]
83 |
84 | if item[0] == 'status':
85 | self.status = item[1]
86 |
87 |
88 | def To_PCB(self, fp = False):
89 | if fp:
90 | pcb = ['fp_arc']
91 | else:
92 | pcb = ['gr_arc']
93 |
94 | pcb.append(['start'] + self.start)
95 | pcb.append(['mid'] + self.mid)
96 | pcb.append(['end'] + self.end)
97 | pcb.append(['width', self.width])
98 | pcb.append(['layer', self.layer])
99 | if self.fill:
100 | pcb.append(['fill', self.fill])
101 | if self.tstamp:
102 | pcb.append(['tstamp', self.tstamp])
103 | if self.status:
104 | pcb.append(['status', self.status])
105 |
106 | return pcb
107 |
108 | def To_SVG(self, fp = False):
109 | if fp:
110 | arctype = 'fp_arc'
111 | else:
112 | arctype = 'gr_arc'
113 | # m 486.60713,151.00183 a 9.5535717,9.5535717 0 0 1 -9.55357,9.55357
114 | # (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
115 |
116 | start = [(float(self.start[0]) * pxToMM), (float(self.start[1]) * pxToMM)]
117 | mid = [(float(self.mid[0]) * pxToMM), (float(self.mid[1]) * pxToMM)]
118 | end = [(float(self.end[0]) * pxToMM), (float(self.end[1]) * pxToMM)]
119 |
120 | a = self.calcCirclePath(start, end, mid)
121 |
122 | tstamp = ''
123 | status = ''
124 | fill = ''
125 | if self.fill != '':
126 | fill = 'fill="' + self.fill + '" '
127 | if self.tstamp != '':
128 | tstamp = 'tstamp="' + self.tstamp + '" '
129 | if self.status != '':
130 | status = 'status="' + str(self.status) + '" '
131 |
132 | parameters = ''
144 |
145 | return parameters
146 |
147 |
148 | def From_SVG(self, tag, segment):
149 | path = parse_path(tag['d'])
150 | style = tag['style']
151 |
152 | width = style[style.find('stroke-width:') + 13:]
153 | self.width = width[0:width.find('mm')]
154 |
155 | if tag.has_attr('layer'):
156 | self.layer = tag['layer']
157 | elif tag.parent.has_attr('inkscape:label'):
158 | #XML metadata trashed, try to recover from parent tag
159 | self.layer = tag.parent['inkscape:label']
160 | else:
161 | assert False, "Arc not in layer"
162 |
163 | self.start = [str(path[0].start.real / pxToMM), str(path[0].start.imag / pxToMM)]
164 | self.mid = [str(path[0].point(0.5).real / pxToMM), str(path[0].point(0.5).imag / pxToMM)]
165 | self.end = [str(path[0].point(1).real / pxToMM), str(path[0].point(1).imag / pxToMM)]
166 |
167 | if tag.has_attr('fill') == True:
168 | self.fill = tag['fill']
169 |
170 | if tag.has_attr('status') == True:
171 | self.status = tag['status']
172 |
173 | if tag.has_attr('tstamp') == True:
174 | self.tstamp = tag['tstamp']
175 |
176 | def calcCirclePath(self, a, b, c):
177 | def dist(a, b):
178 | return math.sqrt(math.pow(a[0] - b[0], 2) + math.pow(a[1] - b[1], 2))
179 |
180 |
181 | A = dist(b, c)
182 | B = dist(c, a)
183 | C = dist(a, b)
184 |
185 | angle = math.acos((A*A + B*B - C*C)/(2*A*B))
186 |
187 | center = [0,0]
188 |
189 | temp = b[0]*b[0]+b[1]*b[1]
190 | bc = (a[0]*a[0] + a[1]*a[1] - temp)/2.0
191 | cd = (temp - c[0]*c[0] - c[1]*c[1])/2.0
192 | det = (a[0]-b[0])*(b[1]-c[1])-(b[0]-c[0])*(a[1]-b[1])
193 |
194 | if (abs(det) < 1.0e-6):
195 | center[0] = 1.0
196 | center[1] = 1.0
197 | else:
198 | det = 1/det
199 | center[0] = (bc*(b[1]-c[1])-cd*(a[1]-b[1]))*det
200 | center[1] = ((a[0]-b[0])*cd-(b[0]-c[0])*bc)*det
201 | r = math.sqrt((b[0]-center[0])*(b[0]-center[0])+(b[1]-center[1])*(b[1]-center[1]))
202 |
203 | #large arc flag
204 | if math.pi/2 > angle:
205 | laf = '1'
206 | else:
207 | laf = '0'
208 |
209 | #sweep flag
210 | if ((b[0] - a[0])*(c[1] - a[1]) - (b[1] - a[1])*(c[0] - a[0])) < 0:
211 | saf = '1'
212 | else:
213 | saf = '0'
214 |
215 | svg = ' '.join(['M', str(a[0]), str(a[1]), 'A', str(r), str(r), '0', laf, saf, str(b[0]), str(b[1])])
216 |
217 | return svg
--------------------------------------------------------------------------------
/Stretch/kiplug/board.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 |
4 | from bs4 import BeautifulSoup
5 |
6 | import json
7 | from .svgpath import parse_path
8 |
9 | from .arc import Arc
10 | from .circle import Circle
11 | from .curve import Curve
12 | from .layers import Layers
13 | from .line import Line
14 | from .metadata import Metadata
15 | from .module import Module
16 | from .pad import Pad
17 | from .poly import Poly
18 | from .segment import Segment
19 | from .text import Text
20 | from .via import Via
21 | from .zone import Zone
22 |
23 | #https://github.com/KiCad/kicad-source-mirror/blob/93466fa1653191104c5e13231dfdc1640b272777/pcbnew/plugins/kicad/pcb_parser.cpp#L533
24 |
25 | # kicad_pcb
26 | # version
27 | # host
28 | # general
29 | # page
30 | # title_block
31 | # layers
32 | # setup
33 | # net
34 | # net_class
35 | # module
36 | # dimension
37 | # gr_line
38 | # gr_arc
39 | # gr_text
40 | # segment
41 | # via
42 | # zone
43 |
44 | #Prettifies SVG output, but messes up text field spacing
45 | debug = False
46 |
47 | class Board(object):
48 |
49 | def __init__(self):
50 | self.clear()
51 |
52 | def clear(self):
53 | self.general = ''
54 | self.paper = ''
55 | self.title_block = ''
56 |
57 |
58 | self.layers = Layers()
59 | # self.layers = ''
60 | self.setup = ''
61 | self.property = ''
62 | self.net = ''
63 | self.net_class = ''
64 | self.gr_arc = []
65 | self.gr_curve = []
66 | self.gr_line = []
67 | self.gr_poly = []
68 | self.gr_circle = []
69 | self.gr_rect = []
70 | self.gr_text = []
71 | self.gr_dimension = []
72 | self.module = []
73 | self.footprint = []
74 | self.segment = []
75 | self.arc = []
76 | self.group = ''
77 | self.via = []
78 | self.zone = []
79 | self.target = ''
80 | self.metadata = []
81 |
82 |
83 | def From_PCB(self, pcb):
84 |
85 | for item in pcb:
86 | if type(item) is str:
87 | print(item)
88 | else:
89 |
90 | if item[0] == 'layers':
91 | self.layers = Layers()
92 | self.layers.From_PCB(item)
93 |
94 | elif item[0] == 'footprint':
95 | # This is the new name of modules
96 | # KiCad 6 supports "module" as a legacy option
97 | # So that's what we will use.
98 | module = Module()
99 | module.From_PCB(item)
100 | self.module.append(module)
101 |
102 | elif item[0] == 'module':
103 | module = Module()
104 | module.From_PCB(item)
105 | self.module.append(module)
106 |
107 | elif item[0] == 'segment':
108 | segment = Segment()
109 | segment.From_PCB(item)
110 | self.segment.append(segment)
111 |
112 | elif item[0] == 'arc':
113 | arc = Arc()
114 | arc.From_PCB(item)
115 | self.arc.append(arc)
116 |
117 | elif item[0] == 'gr_arc':
118 | arc = Arc()
119 | arc.From_PCB(item)
120 | self.gr_arc.append(arc)
121 |
122 | elif item[0] == 'gr_line':
123 | line = Line()
124 | line.From_PCB(item)
125 | self.gr_line.append(line)
126 |
127 | elif item[0] == 'gr_circle':
128 | circle = Circle()
129 | circle.From_PCB(item)
130 | self.gr_circle.append(circle)
131 |
132 | elif item[0] == 'gr_poly':
133 | poly = Poly()
134 | poly.From_PCB(item)
135 | self.gr_poly.append(poly)
136 |
137 | elif item[0] == 'gr_curve':
138 | curve = Curve()
139 | curve.From_PCB(item)
140 | self.gr_curve.append(curve)
141 |
142 | elif item[0] == 'gr_text':
143 | text = Text()
144 | text.From_PCB(item)
145 | self.gr_text.append(text)
146 | # tag = BeautifulSoup(self.Convert_Gr_Text_To_SVG(item, i), 'html.parser')
147 | # layer = tag.find('text')['layer']
148 | # base.svg.find('g', {'inkscape:label': layer}, recursive=False).append(tag)
149 |
150 | elif item[0] == 'zone':
151 | zone = Zone()
152 | zone.From_PCB(item)
153 | self.zone.append(zone)
154 |
155 | elif item[0] == 'via':
156 | via = Via()
157 | via.From_PCB(item)
158 | self.via.append(via)
159 |
160 | else:
161 | # Numeric non-integer has to be tricked
162 | if item[0] == 'generator_version':
163 | item[1] = str(item[1]) + ' '
164 | self.metadata.append(item)
165 |
166 |
167 | def To_PCB(self):
168 | pcb = ['kicad_pcb']
169 | pcb += self.metadata
170 |
171 | pcb.append(self.layers.To_PCB())
172 |
173 | for item in self.module:
174 | pcb.append(item.To_PCB())
175 |
176 | for item in self.segment:
177 | pcb.append(item.To_PCB())
178 |
179 | for item in self.arc:
180 | pcb.append(item.To_PCB())
181 |
182 | for item in self.gr_arc:
183 | pcb.append(item.To_PCB())
184 |
185 | for item in self.gr_line:
186 | pcb.append(item.To_PCB())
187 |
188 | for item in self.gr_circle:
189 | pcb.append(item.To_PCB())
190 |
191 | for item in self.gr_poly:
192 | pcb.append(item.To_PCB())
193 |
194 | for item in self.gr_curve:
195 | pcb.append(item.To_PCB())
196 |
197 | for item in self.gr_text:
198 | pcb.append(item.To_PCB())
199 |
200 | for item in self.zone:
201 | pcb.append(item.To_PCB())
202 |
203 | for item in self.via:
204 | pcb.append(item.To_PCB())
205 |
206 |
207 | # print(pcb)
208 | return pcb
209 |
210 |
211 | def To_SVG(self):
212 |
213 | base = BeautifulSoup(base_proto, 'html.parser')
214 |
215 | base.svg.append(BeautifulSoup('', 'html.parser'))
216 |
217 |
218 | layers = self.layers.To_SVG()
219 |
220 | for layer in layers:
221 | tag = BeautifulSoup(layer, 'html.parser')
222 | base.svg.append(tag)
223 |
224 | hiddenLayers = []
225 | for layer in self.layers.layer:
226 | if 'hide' in layer[2:]:
227 | hiddenLayers.append(layer[1])
228 |
229 | base.svg.append(BeautifulSoup('', 'html.parser'))
230 | base.svg.append(BeautifulSoup('', 'html.parser'))
231 | base.svg.append(BeautifulSoup('', 'html.parser'))
232 |
233 |
234 | for item in self.segment:
235 | tag = BeautifulSoup(item.To_SVG(), 'html.parser')
236 | layer = item.layer
237 |
238 | if base.svg.find('g', {'inkscape:label': layer}, recursive=False):
239 | base.svg.find('g', {'inkscape:label': layer}, recursive=False).append(tag)
240 | else:
241 | print(base.svg)
242 | print('---')
243 | print(layer)
244 | print('---')
245 | print(base.svg.find('g', {'inkscape:label': layer}, recursive=False))
246 | print(base.svg.find('g', {'inkscape:label': layer}, recursive=True))
247 | print('--->')
248 |
249 | for item in self.arc:
250 | tag = BeautifulSoup(item.To_SVG(), 'html.parser')
251 | layer = item.layer
252 | base.svg.find('g', {'inkscape:label': layer}, recursive=False).append(tag)
253 |
254 | for item in self.module:
255 | tag = item.To_SVG(hiddenLayers = hiddenLayers)
256 | # layer = item.layer
257 | # print(base.svg)
258 | # print('---')
259 | # print(base.svg.find('g', {'inkscape:label': 'Modules'}, recursive=False))
260 | if base.svg.find('g', {'inkscape:label': 'Modules'}, recursive=False):
261 | base.svg.find('g', {'inkscape:label': 'Modules'}, recursive=False).append(tag)
262 | else:
263 | print(base.svg)
264 | print('---')
265 | print(base.svg.find('g', {'inkscape:label': 'Modules'}, recursive=False))
266 | print(base.svg.find('g', {'inkscape:label': 'Modules'}, recursive=True))
267 | print('--->')
268 |
269 | # try:
270 | # base.svg.find('g', {'inkscape:label': 'Modules'}, recursive=False).append(tag)
271 | # except:
272 | # print(base.svg)
273 | # print('---')
274 | # print(base.svg.find('g', {'inkscape:label': 'Modules'}, recursive=False))
275 |
276 |
277 | for item in self.gr_poly:
278 | tag = BeautifulSoup(item.To_SVG(), 'html.parser')
279 | layer = item.layer
280 | base.svg.find('g', {'inkscape:label': layer}, recursive=False).append(tag)
281 |
282 | for item in self.gr_line:
283 | tag = BeautifulSoup(item.To_SVG(), 'html.parser')
284 | layer = item.layer
285 | base.svg.find('g', {'inkscape:label': layer}, recursive=False).append(tag)
286 |
287 | for item in self.gr_arc:
288 | tag = BeautifulSoup(item.To_SVG(), 'html.parser')
289 | layer = item.layer
290 | base.svg.find('g', {'inkscape:label': layer}, recursive=False).append(tag)
291 |
292 | for item in self.gr_curve:
293 | tag = BeautifulSoup(item.To_SVG(), 'html.parser')
294 | layer = item.layer
295 | base.svg.find('g', {'inkscape:label': layer}, recursive=False).append(tag)
296 |
297 | for item in self.gr_circle:
298 | tag = BeautifulSoup(item.To_SVG(), 'html.parser')
299 | layer = item.layer
300 | base.svg.find('g', {'inkscape:label': layer}, recursive=False).append(tag)
301 |
302 | for item in self.gr_text:
303 | tag = BeautifulSoup(item.To_SVG(), 'html.parser')
304 | layer = item.layer
305 | base.svg.find('g', {'inkscape:label': layer}, recursive=False).append(tag)
306 |
307 | for item in self.via:
308 | tag = BeautifulSoup(item.To_SVG(), 'html.parser')
309 | base.svg.find('g', {'inkscape:label': 'Vias'}, recursive=False).append(tag)
310 |
311 | for item in self.zone:
312 | tag = BeautifulSoup(item.To_SVG(), 'html.parser')
313 | base.svg.find('g', {'inkscape:label': 'Zones'}, recursive=False).append(tag)
314 |
315 | for item in self.metadata:
316 | tag = BeautifulSoup(Metadata().To_SVG(item), 'html.parser')
317 | base.svg.kicad.append(tag)
318 |
319 | if debug == True:
320 | svg = base.prettify("utf-8")
321 | else:
322 | svg = base.encode()
323 |
324 | return svg
325 |
326 |
327 | def From_SVG(self, svg):
328 | self.layers = Layers()
329 | self.layers.From_SVG(svg)
330 |
331 | metadata = Metadata()
332 | self.metadata = metadata.From_SVG(svg)
333 |
334 | for tag in svg.svg.find_all('g'):
335 | if tag.has_attr('type') == True:
336 | if tag['type'] == 'layervia':
337 | for viatag in tag.find_all('g'):
338 | via = Via()
339 | via.From_SVG(viatag)
340 | self.via.append(via)
341 |
342 | elif tag['type'] == "module":
343 | for moduletag in tag.find_all('g'):
344 | module = Module()
345 | module.From_SVG(moduletag)
346 | self.module.append(module)
347 |
348 | # Pass on solving this here
349 | # elif tag['type'] == "layerzone":
350 |
351 | # for zonetag in tag.find_all('g'):
352 | # paths = parse_path(zonetag['d'])
353 | # zone = Zone()
354 | # zone.From_SVG(zonetag, paths)
355 | # self.zone.append(zone)
356 |
357 | for tag in svg.svg.find_all('text'):
358 | if tag.has_attr('type') == True:
359 | if tag['type'] == "gr_text":
360 | t = Text()
361 | t.From_SVG(tag)
362 | self.gr_text.append(t)
363 |
364 | for tag in svg.svg.find_all('path'):
365 | if tag.has_attr('type') == True:
366 |
367 | if tag['type'] == "segment":
368 | paths = parse_path(tag['d'])
369 |
370 | for path in paths:
371 | segment = Segment()
372 | segment.From_SVG(tag, path)
373 | self.segment.append(segment)
374 |
375 | elif tag['type'] == "arc":
376 | paths = parse_path(tag['d'])
377 |
378 | for path in paths:
379 | arc = Arc()
380 | arc.From_SVG(tag, path)
381 | self.arc.append(arc)
382 |
383 |
384 | elif tag['type'] == "gr_line":
385 | paths = parse_path(tag['d'])
386 |
387 | for path in paths:
388 | line = Line()
389 | line.From_SVG(tag, path)
390 | self.gr_line.append(line)
391 |
392 | elif tag['type'] == "gr_arc":
393 |
394 | print(tag)
395 | paths = parse_path(tag['d'])
396 |
397 | for path in paths:
398 | arc = Arc()
399 | arc.From_SVG(tag, path)
400 | self.gr_arc.append(arc)
401 |
402 | elif tag['type'] == "gr_curve":
403 | paths = parse_path(tag['d'])
404 |
405 | for path in paths:
406 | print(path)
407 | curve = Curve()
408 | curve.From_SVG(tag)
409 | self.gr_curve.append(curve)
410 |
411 | elif tag['type'] == "gr_poly":
412 | paths = parse_path(tag['d'])
413 | poly = Poly()
414 | poly.From_SVG(tag)
415 | self.gr_poly.append(poly)
416 |
417 | elif tag['type'] == "zone":
418 | paths = parse_path(tag['d'])
419 | zone = Zone()
420 | zone.From_SVG(tag, paths)
421 | self.zone.append(zone)
422 |
423 |
424 | base_proto = '''
425 |
426 |
427 |
428 |
476 | '''
--------------------------------------------------------------------------------
/Stretch/kiplug/circle.py:
--------------------------------------------------------------------------------
1 |
2 | import math
3 | from .colour import Colour
4 |
5 | # https://github.com/KiCad/kicad-source-mirror/blob/93466fa1653191104c5e13231dfdc1640b272777/pcbnew/plugins/kicad/pcb_parser.cpp#L2209
6 |
7 |
8 | # 0 gr_circle
9 | # 1
10 | # 0 center
11 | # 1 66.66
12 | # 2 99.99
13 | # 2
14 | # 0 end
15 | # 1 66.66
16 | # 2 99.99
17 | # 3
18 | # 0 layer
19 | # 1 Edge.Cuts
20 | # 4
21 | # 0 width
22 | # 1 0.05
23 | # 5
24 | # 0 tstamp
25 | # 1 5E451B20
26 |
27 |
28 | pxToMM = 96 / 25.4
29 |
30 |
31 | class Circle(object):
32 |
33 | def __init__(self):
34 | self.center = []
35 | self.end = []
36 | self.width = '0'
37 | self.layer = ''
38 | self.fill = ''
39 | self.tstamp = ''
40 | self.status = ''
41 |
42 |
43 | def From_PCB(self, input):
44 |
45 |
46 | start = []
47 | end = []
48 |
49 | for item in input:
50 | if type(item) == str:
51 | continue
52 |
53 | if item[0] == 'center':
54 | self.center.append(float(item[1]))
55 | self.center.append(float(item[2]))
56 |
57 | if item[0] == 'end':
58 | self.end.append(float(item[1]))
59 | self.end.append(float(item[2]))
60 |
61 | # if item[0] == 'angle':
62 | # self.angle = item[1]
63 | # assert False,"Gr_circle: Please report this! Never seen before."
64 |
65 | if item[0] == 'layer':
66 | self.layer = item[1]
67 |
68 | if item[0] == 'width':
69 | self.width = item[1]
70 |
71 | if item[0] == 'fill':
72 | self.fill = item[1]
73 |
74 | if item[0] == 'tstamp':
75 | self.tstamp = item[1]
76 |
77 | if item[0] == 'status':
78 | self.status = item[1]
79 |
80 | def To_PCB(self, fp = False):
81 | pcb = []
82 | if fp:
83 | pcb = ['fp_circle']
84 | else:
85 | pcb = ['gr_circle']
86 |
87 | pcb.append(['center'] + self.center)
88 | pcb.append(['end'] + self.end)
89 | pcb.append(['width', self.width])
90 | # pcb.append(['angle', self.angle])
91 | pcb.append(['layer', self.layer])
92 | if self.fill != '':
93 | pcb.append(['fill', self.fill])
94 | pcb.append(['tstamp', self.tstamp])
95 | pcb.append(['status', self.status])
96 |
97 | return pcb
98 |
99 | def To_SVG(self, fp = False):
100 | if fp:
101 | circletype = 'fp_circle'
102 | else:
103 | circletype = 'gr_circle'
104 | tstamp = ''
105 | status = ''
106 | fill = ''
107 |
108 | if self.fill != '':
109 | fill = 'fill="' + self.fill + '" '
110 | if self.tstamp != '':
111 | tstamp = 'tstamp="' + self.tstamp + '" '
112 | if self.status != '':
113 | status = 'status="' + str(self.status) + '" '
114 |
115 | r = abs(math.hypot(self.center[0] - self.end[0], self.center[1] - self.end[1]))
116 |
117 | parameters = ''
130 |
131 | return parameters
132 |
133 |
134 |
135 | def From_SVG(self, tag):
136 | style = tag['style']
137 |
138 | width = style[style.find('stroke-width:') + 13:]
139 | self.width = width[0:width.find('mm')]
140 |
141 | if tag.has_attr('layer'):
142 | self.layer = tag['layer']
143 | elif tag.parent.has_attr('inkscape:label'):
144 | #XML metadata trashed, try to recover from parent tag
145 | self.layer = tag.parent['inkscape:label']
146 | else:
147 | assert False, "Circle not in layer"
148 |
149 |
150 | r = str((float(tag['r']) +float(tag['cx'] )) / pxToMM)
151 | x = str(float(tag['cx']) / pxToMM)
152 | y = str(float(tag['cy']) / pxToMM)
153 | self.center = [x, y]
154 | self.end = [r, y]
155 |
156 | if tag.has_attr('fill') == True:
157 | self.fill = tag['fill']
158 |
159 | if tag.has_attr('status') == True:
160 | self.status = tag['status']
161 |
162 | if tag.has_attr('tstamp') == True:
163 | self.tstamp = tag['tstamp']
164 |
165 |
166 |
--------------------------------------------------------------------------------
/Stretch/kiplug/colour.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | class Colour(object):
4 |
5 | def Assign(self, layername):
6 | if sys.version_info[0] != 3:
7 | if type(layername) == unicode:
8 | layername = str(layername)
9 | colours = {
10 | 'F.Cu': '840000',
11 | 'In1.Cu': 'C2C200',
12 | 'In2.Cu': 'C200C2',
13 | 'B.Cu': '008400',
14 | 'B.Adhes': '840084',
15 | 'F.Adhes': '000084',
16 | 'B.Paste': '000084',
17 | 'F.Paste': '840000',
18 | 'B.SilkS': '840084',
19 | 'F.SilkS': '008484',
20 | 'B.Mask': '848400',
21 | 'F.Mask': '840084',
22 | 'Dwgs.User': 'c2c2c2',
23 | 'Cmts.User': '000084',
24 | 'Eco1.User': '008400',
25 | 'Eco2.User': 'c2c200',
26 | 'Edge.Cuts': 'C2C200',
27 | 'Margin': 'c200c2',
28 | 'B.CrtYd': '848484',
29 | 'F.CrtYd': 'c2c2c2',
30 | 'B.Fab': '000084',
31 | 'F.Fab': '848484',
32 | 'Via.Outer': 'c2c2c2',
33 | 'Via.Inner': '8c7827',
34 | '*.Cu': '840000',
35 | 'Default': 'FFFF00'
36 | }
37 |
38 | if layername in colours:
39 | return colours[layername]
40 | else:
41 | return colours['Default']
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Stretch/kiplug/curve.py:
--------------------------------------------------------------------------------
1 |
2 | from .colour import Colour
3 | from .svgpath import parse_path
4 |
5 | # 0 gr_curve
6 | # 1
7 | # 0 pts
8 | # 1
9 | # 0 xy
10 | # 1 99.99
11 | # 2 99.99
12 | # 2
13 | # 0 xy
14 | # 1 99.99
15 | # 2 99.99
16 | # 3
17 | # 0 xy
18 | # 1 99.99
19 | # 2 99.99
20 | # 4
21 | # 0 xy
22 | # 1 99.99
23 | # 2 99.99
24 | # 2
25 | # 0 layer
26 | # 1 Edge.Cuts
27 | # 3
28 | # 0 width
29 | # 1 0.05
30 | # 4
31 | # 0 tstamp
32 | # 1 5E451B20
33 |
34 | pxToMM = 96 / 25.4
35 |
36 |
37 | class Curve(object):
38 |
39 | def __init__(self):
40 | self.pts = []
41 | self.width = 0
42 | self.layer = ''
43 | self.fill = ''
44 | self.tstamp = ''
45 | self.status = ''
46 |
47 | def From_PCB(self, input):
48 |
49 | for item in input:
50 | if item[0] == 'pts':
51 | for xy in item:
52 | if xy[0] == 'xy':
53 | self.pts.append([xy[1], xy[2]])
54 |
55 | if item[0] == 'layer':
56 | self.layer = item[1]
57 |
58 | if item[0] == 'width':
59 | self.width = item[1]
60 |
61 | if item[0] == 'fill':
62 | self.fill = item[1]
63 |
64 | if item[0] == 'tstamp':
65 | self.tstamp = item[1]
66 |
67 | if item[0] == 'status':
68 | self.status = item[1]
69 |
70 | def To_PCB(self, fp = False):
71 | pcb = []
72 | if fp:
73 | pcb = ['fp_curve']
74 | else:
75 | pcb = ['gr_curve']
76 |
77 | pts = ['pts']
78 |
79 | for item in self.pts:
80 | xy = ['xy'] + item
81 | pts += [xy]
82 |
83 | pcb.append(pts)
84 | pcb.append(['layer', self.layer])
85 | pcb.append(['width', self.width])
86 | # pcb.append(['fill', self.fill])
87 | pcb.append(['tstamp', self.tstamp])
88 | pcb.append(['status', self.status])
89 |
90 | return pcb
91 |
92 | def To_SVG(self, fp = False):
93 | if fp:
94 | polytype = 'fp_curve'
95 | else:
96 | polytype = 'gr_curve'
97 |
98 |
99 | points = []
100 | tstamp = ''
101 | status = ''
102 | fill = ''
103 |
104 | #This might have a problem with random list ordering in certain versions of Python
105 | for xy in self.pts:
106 | points.append(float(xy[0]))
107 | points.append(float(xy[1]))
108 |
109 | if self.tstamp != '':
110 | tstamp = 'tstamp="' + self.tstamp + '" '
111 | if self.status != '':
112 | status = 'status="' + self.status + '" '
113 | if self.fill != '':
114 | fill = 'fill="' + self.fill + '" '
115 |
116 |
117 | parameters = ''
131 |
132 | return parameters
133 |
134 |
135 | def From_SVG(self, tag):
136 | style = tag['style']
137 |
138 | styletag = style[style.find('stroke-width:') + 13:]
139 | width = styletag[0:styletag.find('mm')]
140 |
141 | if tag.has_attr('layer'):
142 | layer = tag['layer']
143 | elif tag.parent.has_attr('inkscape:label'):
144 | #XML metadata trashed, try to recover from parent tag
145 | layer = tag.parent['inkscape:label']
146 | else:
147 | assert False, "Curve not in layer"
148 |
149 | #Todo: fix up this...
150 | #----------------
151 | # path = parse_path(tag['d'])
152 |
153 | # pts = []
154 | # for point in path:
155 | # xy = []
156 | # xy.append(str(point.start.real / pxToMM))
157 | # xy.append(str(point.start.imag / pxToMM))
158 | # pts.append(xy)
159 |
160 | # xy = []
161 | # xy.append(str(path[0].start.real / pxToMM))
162 | # xy.append(str(path[0].start.imag / pxToMM))
163 | # pts.append(xy)
164 | #----------------
165 | #Because this old method is real bad:
166 |
167 | xy_float = 4 * [0.0]
168 |
169 | unparsed_path = tag['d'].split(' ')
170 | xy_str = unparsed_path[1].split(',')
171 | xy_float[0] = [float(xy_str[0]), float(xy_str[1])]
172 | xy_str = unparsed_path[3].split(',')
173 | xy_float[1] = [float(xy_str[0]), float(xy_str[1])]
174 | xy_str = unparsed_path[4].split(',')
175 | xy_float[2] = [float(xy_str[0]), float(xy_str[1])]
176 | xy_str = unparsed_path[5].split(',')
177 | xy_float[3] = [float(xy_str[0]), float(xy_str[1])]
178 |
179 |
180 | #relative / absolute compensation
181 | if unparsed_path[2] == 'c':
182 | xy_float[1][0] = xy_float[0][0] - xy_float[1][0] * -1
183 | xy_float[1][1] = xy_float[0][1] - xy_float[1][1] * -1
184 | xy_float[2][0] = xy_float[0][0] - xy_float[2][0] * -1
185 | xy_float[2][1] = xy_float[0][1] - xy_float[2][1] * -1
186 | xy_float[3][0] = xy_float[0][0] - xy_float[3][0] * -1
187 | xy_float[3][1] = xy_float[0][1] - xy_float[3][1] * -1
188 |
189 |
190 | xy = [str(xy_float[0][0] / pxToMM), str(xy_float[0][1] / pxToMM)]
191 |
192 | pts = [xy]
193 |
194 | xy = [str(xy_float[1][0] / pxToMM), str(xy_float[1][1] / pxToMM)]
195 | pts.append(xy)
196 |
197 | xy = [str(xy_float[2][0] / pxToMM), str(xy_float[2][1] / pxToMM)]
198 | pts.append(xy)
199 |
200 | xy = [str(xy_float[3][0] / pxToMM), str(xy_float[3][1] / pxToMM)]
201 | pts.append(xy)
202 |
203 |
204 | self.pts = pts
205 | self.width = width
206 | self.layer = layer
207 |
208 | if tag.has_attr('tstamp'):
209 | self.tstamp = tag['tstamp']
210 | if tag.has_attr('fill'):
211 | self.fill = tag['fill']
212 | if tag.has_attr('status'):
213 | self.status = tag['status']
214 |
215 |
--------------------------------------------------------------------------------
/Stretch/kiplug/kicad.py:
--------------------------------------------------------------------------------
1 | import os, shutil
2 | from datetime import datetime
3 |
4 | import pcbnew
5 | import sys
6 |
7 | class Stretch(pcbnew.ActionPlugin, object):
8 |
9 | def __init__(self, tool):
10 | self.tool = tool
11 | super(Stretch, self).__init__()
12 |
13 | def defaults(self):
14 | if self.tool == "to_svg":
15 | self.name = "Stretch-To-SVG"
16 | self.category = "A KiCad plugin"
17 | self.description = "A plugin to add beauty"
18 | self.show_toolbar_button = True # Optional, defaults to False
19 | self.icon_file_name = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'icons', 'to_svg.png') # Optional
20 | elif self.tool == "to_pcb":
21 | self.name = "Stretch-To-PCB"
22 | self.category = "A KiCad plugin"
23 | self.description = "A plugin to add beauty"
24 | self.show_toolbar_button = True # Optional, defaults to False
25 | self.icon_file_name = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'icons', 'to_pcb.png') # Optional
26 |
27 | self.svg_file_name = 'out.svg'
28 |
29 | def Backup(self, filename):
30 | # Backups are for when the plugin or the person does an oopsie
31 | # Running Stretch-from-SVG on a main file will act normally
32 | # but also create a filename.stretch_bkup.kicad_pcb copy beforehand.
33 | # Running Stretch-from-SVG on a backup will leave the backup untouched
34 | # and overwrite the main file with the new SVG->PCB data
35 |
36 | head, tail = os.path.split(filename)
37 | extension_name = ".kicad_pcb"
38 | backup_name = ".stretch_bkup"
39 |
40 | base_filename, ext = os.path.splitext(tail)
41 | if ext == extension_name:
42 | base_filename, bckup = os.path.splitext(base_filename)
43 | if bckup == backup_name:
44 | # this is already a backup, return filename without backup suffix
45 | return os.path.join(head, base_filename + extension_name)
46 | else:
47 | # copy file to backup, then return original filename unchanged
48 | dst_file = os.path.join(head, base_filename + backup_name + extension_name)
49 |
50 | if os.path.exists(dst_file):
51 | os.remove(dst_file)
52 | shutil.move(filename, dst_file)
53 |
54 | return filename
55 | else:
56 | #This is an error condition
57 | print('Unrecognised extension: ', filename, tail, base_filename, ext)
58 | return filename
59 |
60 | def Run(self):
61 | b = pcbnew.GetBoard()
62 | pcb_filename = b.GetFileName()
63 |
64 | sys.stdout = open(os.path.join(os.path.dirname(pcb_filename), "out.log"), 'w')
65 | print('Begin Stretch debug')
66 |
67 | print('Import BS4')
68 |
69 | try:
70 | from bs4 import BeautifulSoup
71 | except ModuleNotFoundError:
72 | python_path = os.path.join(os.path.dirname(os.path.dirname(os.__file__)), "python")
73 | if os.name == "nt":
74 | python_path += ".exe"
75 | bs4_install = '''\n
76 | ********************************************************
77 | **You must install the Python BeautifulSoup package!**
78 | ********************************************************\n
79 | Type the following (or something similar) on your system terminal to install it:\n\n''' \
80 | + python_path + ' -m pip install beautifulsoup4'
81 | raise ModuleNotFoundError(bs4_install) from None
82 |
83 |
84 | print('Success')
85 |
86 | if self.tool == "to_svg":
87 | from .svg_writer import SvgWrite
88 | a = SvgWrite()
89 | a.Run_Plugin(pcb_filename, self.svg_file_name)
90 | elif self.tool == "to_pcb":
91 | from .pcb_writer import PcbWrite
92 | a = PcbWrite()
93 | pcb_filename = self.Backup(pcb_filename)
94 | a.Run_Plugin(pcb_filename, self.svg_file_name)
95 | pcbnew.Refresh()
96 | sys.stdout.close()
97 |
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/Stretch/kiplug/layers.py:
--------------------------------------------------------------------------------
1 | #https://github.com/KiCad/kicad-source-mirror/blob/93466fa1653191104c5e13231dfdc1640b272777/pcbnew/plugins/kicad/pcb_parser.cpp#L1360
2 |
3 | # 0 layers
4 | # 1
5 | # 0 1-whatever layerid
6 | # 1 F.Cu
7 | # 2/3 user/hide(optional)
8 | # 2 ...
9 | # 3 ...
10 |
11 | class Layers(object):
12 |
13 | def __init__(self):
14 | self.layer = []
15 |
16 |
17 | def From_PCB(self, pcblist):
18 |
19 | if pcblist[0] != 'layers':
20 | assert False,"Layers: Not a layer"
21 | return None
22 |
23 | for item in pcblist:
24 |
25 | if item == 'layers':
26 | continue
27 |
28 | layer = [str(item[0]), item[1]]
29 |
30 | if 'user' in item:
31 | layer.append('user')
32 | if 'signal' in item:
33 | layer.append('signal')
34 | if 'power' in item:
35 | layer.append('power')
36 | if 'mixed' in item:
37 | layer.append('mixed')
38 | if 'hide' in item:
39 | layer.append('hide')
40 |
41 | self.layer.append(layer)
42 |
43 | def To_PCB(self):
44 |
45 | layers = []
46 |
47 | for layer in self.layer:
48 | # layer = layername
49 | # layer += self.attribs[layernum]
50 | layers.append(layer)
51 |
52 | layers.append('layers')
53 | layers.reverse()
54 |
55 | return layers
56 |
57 | def From_SVG(self, svg):
58 |
59 | for tag in svg.svg.find_all('g'):
60 | if tag.has_attr('id') == True:
61 | if tag['id'].startswith('layer'):
62 | if tag.has_attr('number') == True:
63 | layer = [tag['number'], tag['inkscape:label']]
64 |
65 | if tag.has_attr('attribs'):
66 | attribs = tag['attribs'].split(',')
67 | layer += attribs
68 |
69 | self.layer.append(layer)
70 |
71 |
72 | def To_SVG(self):
73 | layers = []
74 |
75 | for item in self.layer:
76 |
77 | hiddenlayer = ''
78 | if 'hide' in item[2:]:
79 | hiddenlayer = "style=display:none "
80 |
81 | parameters = ''
89 |
90 | layers.insert(0, parameters)
91 |
92 | return layers
93 |
--------------------------------------------------------------------------------
/Stretch/kiplug/line.py:
--------------------------------------------------------------------------------
1 |
2 | from .colour import Colour
3 |
4 | # https://github.com/KiCad/kicad-source-mirror/blob/93466fa1653191104c5e13231dfdc1640b272777/pcbnew/plugins/kicad/pcb_parser.cpp#L2209
5 |
6 |
7 | # 0 gr_line
8 | # 1
9 | # 0 start
10 | # 1 66.66
11 | # 2 99.99
12 | # 2
13 | # 0 end
14 | # 1 66.66
15 | # 2 99.99
16 | # 3
17 | # 0 layer
18 | # 1 Edge.Cuts
19 | # 4
20 | # 0 width
21 | # 1 0.05
22 | # 5
23 | # 0 tstamp
24 | # 1 5E451B20
25 |
26 |
27 | pxToMM = 96 / 25.4
28 |
29 |
30 | class Line(object):
31 |
32 | def __init__(self):
33 | self.start = []
34 | self.end = []
35 | self.width = 0
36 | self.layer = ''
37 | self.fill = ''
38 | self.tstamp = ''
39 | self.status = ''
40 |
41 |
42 | def From_PCB(self, input):
43 |
44 |
45 | start = []
46 | end = []
47 |
48 | for item in input:
49 | if type(item) == str:
50 | #if item == 'gr_line' or item == 'fp_line':
51 | continue
52 |
53 | if item[0] == 'start':
54 | self.start.append(item[1])
55 | self.start.append(item[2])
56 |
57 | if item[0] == 'end':
58 | self.end.append(item[1])
59 | self.end.append(item[2])
60 |
61 | if item[0] == 'layer':
62 | self.layer = item[1]
63 |
64 | if item[0] == 'width':
65 | self.width = item[1]
66 |
67 | if item[0] == 'fill':
68 | self.fill = item[1]
69 |
70 | if item[0] == 'tstamp':
71 | self.tstamp = item[1]
72 |
73 | if item[0] == 'status':
74 | self.status = item[1]
75 |
76 | def To_PCB(self, fp = False):
77 | pcb = []
78 | if fp:
79 | pcb = ['fp_line']
80 | else:
81 | pcb = ['gr_line']
82 |
83 | pcb.append(['start'] + self.start)
84 | pcb.append(['end'] + self.end)
85 | pcb.append(['width', self.width])
86 | pcb.append(['layer', self.layer])
87 | if self.fill:
88 | pcb.append(['fill', self.fill])
89 | if self.tstamp:
90 | pcb.append(['tstamp', self.tstamp])
91 | if self.status:
92 | pcb.append(['status', self.status])
93 |
94 | return pcb
95 |
96 | def From_SVG(self, tag, path):
97 | style = tag['style']
98 |
99 | width = style[style.find('stroke-width:') + 13:]
100 | self.width = width[0:width.find('mm')]
101 |
102 | if tag.has_attr('layer'):
103 | self.layer = tag['layer']
104 | elif tag.parent.has_attr('inkscape:label'):
105 | #XML metadata trashed, try to recover from parent tag
106 | self.layer = tag.parent['inkscape:label']
107 | else:
108 | assert False, "Path not in layer"
109 |
110 |
111 | self.start = [str(path.start.real / pxToMM), str(path.start.imag / pxToMM)]
112 | self.end = [str(path.end.real / pxToMM), str(path.end.imag / pxToMM)]
113 |
114 |
115 | if tag.has_attr('fill') == True:
116 | self.fill = tag['fill']
117 |
118 | if tag.has_attr('status') == True:
119 | self.status = tag['status']
120 |
121 | if tag.has_attr('tstamp') == True:
122 | self.tstamp = tag['tstamp']
123 |
124 |
125 |
126 | def To_SVG(self, fp = False):
127 | if fp:
128 | linetype = 'fp_line'
129 | else:
130 | linetype = 'gr_line'
131 |
132 | tstamp = ''
133 | status = ''
134 | fill = ''
135 |
136 | if self.fill != '':
137 | fill = 'fill="' + self.fill + '" '
138 | if self.tstamp != '':
139 | tstamp = 'tstamp="' + self.tstamp + '" '
140 | if self.status != '':
141 | status = 'status="' + str(self.status) + '" '
142 |
143 | parameters = ''
155 |
156 | return parameters
157 |
158 |
--------------------------------------------------------------------------------
/Stretch/kiplug/metadata.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | class Metadata(object):
4 |
5 | def __init__(self):
6 | self.tstamp = ''
7 |
8 |
9 |
10 | # Not used, no processing done
11 | # def From_PCB(self, pcb):
12 | # def To_PCB(self):
13 |
14 | def To_SVG(self, input):
15 | # This will just take whatever data and store it in an XML tag as JSON
16 | # Hacky, but we don't care about it other than to be able to load it back in later
17 |
18 | tag = str(input[0])
19 | input = input[1:]
20 |
21 | body = json.dumps(input)
22 | svg = '<' + tag + '>'
23 | svg += str(body)
24 | svg += '' + tag + '>'
25 |
26 | return svg
27 |
28 |
29 | def From_SVG(self, svg):
30 | # content = svg.svg.kicad.contents[0][0:-1]
31 | pcb = []
32 |
33 | for tag in svg.svg.kicad.children:
34 | if tag.name != None:
35 | chunk = [tag.name]
36 | try:
37 | chunk += json.loads(tag.decode_contents())
38 | except:
39 | assert False,"Bad metadata {}: {} - {}".format(type(tag), tag, chunk)
40 |
41 | pcb += [chunk]
42 | # content = '[' + content + ' ]'
43 | # meta = json.loads(content)
44 | # print(meta)
45 | return pcb
46 |
--------------------------------------------------------------------------------
/Stretch/kiplug/module.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 |
4 | from bs4 import BeautifulSoup
5 |
6 | import base64
7 | import math
8 | from .svgpath import parse_path
9 |
10 | from .arc import Arc
11 | from .circle import Circle
12 | from .curve import Curve
13 | from .layers import Layers
14 | from .line import Line
15 | from .metadata import Metadata
16 | from .pad import Pad
17 | from .poly import Poly
18 | from .segment import Segment
19 | from .text import Text
20 | from .via import Via
21 | from .zone import Zone
22 |
23 |
24 | # https://github.com/KiCad/kicad-source-mirror/blob/93466fa1653191104c5e13231dfdc1640b272777/pcbnew/plugins/kicad/pcb_parser.cpp#L2839
25 |
26 | # 0 module
27 | # 1 Diode_SMD:D_SMD_SOD123
28 | # 2
29 | # 0 layer
30 | # 1 B.Cu
31 | # 3
32 | # 0 tstamp
33 | # 1 0DF
34 | # 4
35 | # 0 at
36 | # 1 66.66
37 | # 2 99.99
38 | # 3
39 | # 0 descr
40 | # 1 0.25
41 | # 4
42 | # 0 tags
43 | # 1 B.Cu
44 | # 5
45 | # 0 path
46 | # 1 1
47 | # 5
48 | # 0 attr
49 | # 1 1
50 | # 5
51 | # 0 fp_text / fp_line / fp_text / pad
52 | # 1 1
53 | #....
54 | #....
55 | # 5
56 | # 0 model
57 | # 1 ${KISYS3DMOD}/Package_TO_SOT_SMD.3dshapes/SOT-23-6.wrl
58 | # 2 offset
59 | # 0 xyz
60 | # 1 0
61 | # 2 0
62 | # 3 0
63 | # 3 scale
64 | # 0 xyz
65 | # 1 1
66 | # 2 1
67 | # 3 1
68 | # 4 rotate
69 | # 0 xyz
70 | # 1 0
71 | # 2 0
72 | # 3 0
73 |
74 |
75 | pxToMM = 96 / 25.4
76 |
77 | class Module(object):
78 |
79 | def __init__(self):
80 | self.symbol = ''
81 | self.version = ''
82 | self.generator = ''
83 | self.locked = False
84 | self.placed = False
85 | self.layer = ''
86 | self.tedit = ''
87 | self.tstamp = ''
88 | self.at = []
89 | self.descr = ''
90 | self.tags = ''
91 | self.property = []
92 | self.path = ''
93 | self.autoplace_cost90 = ''
94 | self.autoplace_cost180 = ''
95 | self.solder_mask_margin = ''
96 | self.solder_paste_margin = ''
97 | self.solder_paste_ratio = ''
98 | self.clearance = ''
99 | self.zone_connect = ''
100 | self.thermal_width = ''
101 | self.thermal_gap = ''
102 | self.attr = ''
103 | self.fp_text = []
104 | self.fp_arc = []
105 | self.fp_circle = []
106 | self.fp_curve = []
107 | self.fp_rect = []
108 | self.fp_line = []
109 | self.fp_poly = []
110 | self.pad = []
111 | self.model = ''
112 | self.zone = []
113 | self.group = ''
114 |
115 |
116 | def From_PCB(self, pcblist):
117 |
118 | # Why is this necessary?
119 | if sys.version_info[0] != 3:
120 | if type(pcblist[1]) == unicode:
121 | pcblist[1] = str(pcblist[1])
122 |
123 | if type(pcblist[1]) != str:
124 | assert False,"Module: Unexpected symbol type {}: {}".format(type(pcblist[1]), pcblist[1])
125 | return None
126 |
127 | self.symbol = pcblist[1]
128 |
129 | for item in pcblist[2:]:
130 |
131 |
132 | if item[0] == 'version':
133 | self.version = item[1]
134 |
135 | if item[0] == 'generator':
136 | self.generator = item[1]
137 |
138 | if item[0] == 'locked':
139 | self.locked = True
140 |
141 | if item[0] == 'placed':
142 | self.placed = True
143 |
144 | if item[0] == 'layer':
145 | self.layer = item[1]
146 |
147 | if item[0] == 'tedit':
148 | self.tedit = item[1]
149 |
150 | if item[0] == 'tstamp':
151 | self.tstamp = item[1]
152 |
153 | if item[0] == 'at':
154 | self.at += item[1:]
155 |
156 | if item[0] == 'descr':
157 | self.descr = item[1]
158 |
159 | if item[0] == 'tags':
160 | self.tags = item[1]
161 |
162 | if item[0] == 'property':
163 | self.property.append(item[1:])
164 |
165 | if item[0] == 'path':
166 | self.path = item[1]
167 |
168 | if item[0] == 'autoplace_cost90':
169 | self.autoplace_cost90 = item[1]
170 |
171 | if item[0] == 'autoplace_cost180':
172 | self.autoplace_cost180 = item[1]
173 |
174 | if item[0] == 'solder_mask_margin':
175 | self.solder_mask_margin = item[1]
176 |
177 | if item[0] == 'solder_paste_margin':
178 | self.solder_paste_margin = item[1]
179 |
180 | if item[0] == 'solder_paste_ratio':
181 | self.solder_paste_ratio = item[1]
182 |
183 | if item[0] == 'clearance':
184 | self.clearance = item[1]
185 |
186 | if item[0] == 'zone_connect':
187 | self.zone_connect = item[1]
188 |
189 | if item[0] == 'thermal_width':
190 | self.thermal_width = item[1]
191 |
192 | if item[0] == 'thermal_gap':
193 | self.thermal_gap = item[1]
194 |
195 | if item[0] == 'attr':
196 | self.attr = ','.join(item[1:])
197 |
198 | if item[0] == 'fp_text':
199 | text = Text()
200 | text.From_PCB(item)
201 | self.fp_text.append(text)
202 |
203 | if item[0] == 'fp_arc':
204 | arc = Arc()
205 | arc.From_PCB(item)
206 | self.fp_arc.append(arc)
207 |
208 | if item[0] == 'fp_circle':
209 | circle = Circle()
210 | circle.From_PCB(item)
211 | self.fp_circle.append(circle)
212 |
213 | # if item[0] == 'fp_circle':
214 | # if item[0] == 'fp_curve':
215 | # if item[0] == 'fp_rect':
216 |
217 | if item[0] == 'fp_line':
218 | line = Line()
219 | line.From_PCB(item)
220 | self.fp_line.append(line)
221 |
222 | if item[0] == 'fp_poly':
223 | poly = Poly()
224 | poly.From_PCB(item)
225 | self.fp_poly.append(poly)
226 |
227 | if item[0] == 'pad':
228 | pad = Pad()
229 | pad.From_PCB(item)
230 | self.pad.append(pad)
231 |
232 | if item[0] == 'model':
233 | hide = ''
234 | model = item[1] + ';'
235 | i = 2
236 | if item[i][0] == 'hide':
237 | if item[i][1] == 'yes':
238 | hide = "hide;"
239 | i += 1
240 | #offset
241 | model += item[i][1][1] + ',' + item[i][1][2] + ',' + item[i][1][3] + ';'
242 | #scale
243 | i += 1
244 | model += item[i][1][1] + ',' + item[i][1][2] + ',' + item[3][1][3] + ';'
245 | #rotate
246 | i += 1
247 | model += item[i][1][1] + ',' + item[i][1][2] + ',' + item[i][1][3] + ';'
248 | self.model = model + hide
249 |
250 | # if item[0] == 'zone':
251 | # if item[0] == 'group':
252 |
253 |
254 | def To_PCB(self):
255 | module = ['module', self.symbol]
256 | if self.version:
257 | module.append(self.version)
258 |
259 | module.append(['layer', self.layer])
260 |
261 | if self.version:
262 | module.append(['version', self.version])
263 |
264 | if self.generator:
265 | module.append(['generator', self.generator])
266 |
267 | module.append(self.at)
268 |
269 | if self.locked == True:
270 | module.append('locked')
271 |
272 | if self.placed == True:
273 | module.append('placed')
274 |
275 | if self.tedit:
276 | module.append(['tedit', self.tedit])
277 |
278 | if self.tstamp:
279 | module.append(['tstamp', self.tstamp])
280 |
281 | if self.descr:
282 | module.append(['descr', self.descr.replace('"', '\\"')])
283 |
284 | if self.tags:
285 | module.append(['tags', self.tags])
286 |
287 | if self.property:
288 | for prop in self.property:
289 | module.append(['property'] + prop)
290 |
291 | # if self.path:
292 | # module.append(['tstamp', self.tstamp])
293 |
294 | if self.autoplace_cost90:
295 | module.append(['autoplace_cost90', self.autoplace_cost90])
296 |
297 | if self.autoplace_cost180:
298 | module.append(['autoplace_cost180', self.autoplace_cost180])
299 |
300 | if self.solder_mask_margin:
301 | module.solder_mask_margin(['solder_mask_margin', self.solder_mask_margin])
302 |
303 | if self.solder_paste_margin:
304 | module.append(['solder_paste_margin', self.solder_paste_margin])
305 |
306 | if self.solder_paste_ratio:
307 | module.append(['solder_paste_ratio', self.solder_paste_ratio])
308 |
309 | if self.clearance:
310 | module.append(['clearance', self.clearance])
311 |
312 | if self.zone_connect:
313 | module.append(['zone_connect', self.zone_connect])
314 |
315 | if self.thermal_width:
316 | module.append(['thermal_width', self.thermal_width])
317 |
318 | if self.thermal_gap:
319 | module.append(['thermal_gap', self.thermal_gap])
320 |
321 | if self.path:
322 | module.append(['path', self.path])
323 |
324 | if self.attr:
325 | attr = self.attr.split(',')
326 | module.append(['attr'] + attr)
327 |
328 | for text in self.fp_text:
329 | module.append(text.To_PCB())
330 |
331 | for poly in self.fp_poly:
332 | module.append(poly.To_PCB(fp = True))
333 |
334 | for line in self.fp_line:
335 | module.append(line.To_PCB(fp = True))
336 |
337 | for arc in self.fp_arc:
338 | module.append(arc.To_PCB(fp = True))
339 |
340 | for curve in self.fp_curve:
341 | module.append(curve.To_PCB(fp = True))
342 |
343 | for circle in self.fp_circle:
344 | module.append(circle.To_PCB(fp = True))
345 |
346 | for pad in self.pad:
347 | module.append(pad.To_PCB())
348 |
349 | if self.model:
350 | module.append(['model'] + self.model)
351 |
352 | if self.group:
353 | module.append(['group', self.group])
354 |
355 |
356 | return module
357 |
358 |
359 |
360 | def To_SVG(self, hiddenLayers = []):
361 | svg = BeautifulSoup('', 'html.parser')
362 |
363 |
364 | if self.version != '':
365 | svg.g['version'] = self.version
366 |
367 | if self.generator != '':
368 | svg.g['generator'] = self.generator
369 |
370 | if self.locked == True:
371 | svg.g['locked'] = 'true'
372 |
373 | if self.placed == True:
374 | svg.g['placed'] = 'true'
375 |
376 | if self.layer != '':
377 | svg.g['layer'] = self.layer
378 | else:
379 | svg.g['layer'] = 'F.Cu'
380 |
381 | if self.tedit != '':
382 | svg.g['tedit'] = self.tedit
383 |
384 | if self.tstamp != '':
385 | svg.g['tstamp'] = self.tstamp
386 |
387 | #at
388 | x = float(self.at[0]) * pxToMM
389 | y = float(self.at[1]) * pxToMM
390 | rotate = 0
391 |
392 | transform = 'translate(' + str(x) + ',' + str(y) + ')'
393 |
394 | if len(self.at) > 2:
395 | rotate = float(self.at[2])
396 | transform += ' rotate(' + str(-1 * rotate) + ')'
397 |
398 | svg.g['transform'] = transform
399 |
400 | if self.descr != '':
401 | svg.g['descr'] = self.descr
402 |
403 | if self.tags != '':
404 | svg.g['tags'] = self.tags
405 |
406 | if len(self.property) > 0:
407 | propstr = ''
408 | for prop in self.property:
409 | propstr += "{}:{} ".format(str(base64.b64encode(prop[0].encode("utf-8")), "utf-8"), str(base64.b64encode(prop[1].encode("utf-8")), "utf-8"))
410 | svg.g['property'] = propstr
411 |
412 | if self.path != '':
413 | svg.g['path'] = self.path
414 |
415 | if self.autoplace_cost90 != '':
416 | svg.g['autoplace_cost90'] = self.autoplace_cost90
417 |
418 | if self.autoplace_cost180 != '':
419 | svg.g['autoplace_cost180'] = self.autoplace_cost180
420 |
421 | if self.solder_mask_margin != '':
422 | svg.g['solder_mask_margin'] = self.solder_mask_margin
423 |
424 | if self.solder_paste_margin != '':
425 | svg.g['solder_paste_margin'] = self.solder_paste_margin
426 |
427 | if self.solder_paste_ratio != '':
428 | svg.g['solder_paste_ratio'] = self.solder_paste_ratio
429 |
430 | if self.solder_paste_ratio != '':
431 | svg.g['solder_paste_ratio'] = self.solder_paste_ratio
432 |
433 | if self.zone_connect != '':
434 | svg.g['zone_connect'] = self.zone_connect
435 |
436 | if self.thermal_width != '':
437 | svg.g['thermal_width'] = self.thermal_width
438 |
439 | if self.thermal_gap != '':
440 | svg.g['thermal_gap'] = self.thermal_gap
441 |
442 | if self.attr != '':
443 | svg.g['attr'] = self.attr
444 |
445 | for item in self.fp_text:
446 | # tag = BeautifulSoup(item.To_SVG(), 'html.parser')
447 | # layer = item.layer
448 | # svg.find('g', {'inkscape:label': layer}, recursive=False).append(tag)
449 | tag = BeautifulSoup(item.To_SVG(rotate * -1, hiddenLayers), 'html.parser')
450 | svg.g.append(tag)
451 | #Todo: hide elements that are supposed to be on hiddenlayers
452 |
453 | for item in self.fp_arc:
454 | tag = BeautifulSoup(item.To_SVG(fp = True), 'html.parser')
455 | svg.g.append(tag)
456 |
457 | for item in self.fp_circle:
458 | tag = BeautifulSoup(item.To_SVG(fp = True), 'html.parser')
459 | svg.g.append(tag)
460 |
461 | for item in self.fp_curve:
462 | tag = BeautifulSoup(item.To_SVG(fp = True), 'html.parser')
463 | svg.g.append(tag)
464 |
465 | for item in self.fp_rect:
466 | tag = BeautifulSoup(item.To_SVG(fp = True), 'html.parser')
467 | svg.g.append(tag)
468 |
469 | for item in self.fp_line:
470 | tag = BeautifulSoup(item.To_SVG(fp = True), 'html.parser')
471 | svg.g.append(tag)
472 |
473 | for item in self.fp_poly:
474 | tag = BeautifulSoup(item.To_SVG(fp = True), 'html.parser')
475 | svg.g.append(tag)
476 |
477 | for item in self.pad:
478 | tag = BeautifulSoup(item.To_SVG(rotate * -1), 'html.parser')
479 | svg.g.append(tag)
480 |
481 | if self.model != '':
482 | svg.g['model'] = self.model
483 | # if self.zone != '':
484 | # if self.group != '':
485 |
486 |
487 | return svg
488 |
489 |
490 |
491 | def From_SVG(self, tag):
492 | transform = tag['transform'].strip()
493 |
494 | x = 0.0
495 | y = 0.0
496 | if 'translate(' in transform:
497 | translate = transform[transform.find('translate(') + 10:-1]
498 | translate = translate[0:translate.find(')')]
499 | x, y = translate.split(',')
500 | x = float(x) / pxToMM
501 | y = float(y) / pxToMM
502 |
503 | rotate = 0.0
504 | if 'rotate(' in transform:
505 | rotate = transform[transform.find('rotate(') + 7:-1]
506 | if ',' in rotate:
507 | rotate, x_zero, y_zero = rotate.split(',')
508 | rotate = float(rotate)
509 | x_zero = float(x_zero) / pxToMM
510 | y_zero = float(y_zero) / pxToMM
511 | #Rotate along nonzero rotation axis
512 | hyp = math.sqrt(x_zero*x_zero + y_zero*y_zero)
513 | angle = -1.0 * math.atan2(-1.0 * y_zero, -1.0 * x_zero)
514 |
515 | angle -= math.radians(rotate)
516 | y_offset = math.sin(angle) * hyp
517 | x_offset = math.cos(angle) * hyp
518 |
519 | x = x_zero + x_offset
520 | y = y_zero - y_offset
521 |
522 | rotate = float(rotate) * -1
523 |
524 |
525 | self.symbol = tag['name']
526 |
527 | if tag.has_attr('layer'):
528 | self.layer = tag['layer']
529 |
530 | if tag.has_attr('tedit'):
531 | self.tedit = tag['tedit']
532 |
533 | if tag.has_attr('property'):
534 | proplist = tag['property'].split()
535 | for prop in proplist:
536 | k, v = prop.split(":")
537 | self.property.append([str(base64.b64decode(k), "utf-8"), str(base64.b64decode(v), "utf-8")])
538 |
539 | if tag.has_attr('tstamp'):
540 | self.tstamp = tag['tstamp']
541 |
542 | at = ['at', str(x), str(y), str(rotate)]
543 | self.at = at
544 |
545 | if tag.has_attr('descr'):
546 | self.descr = tag['descr']
547 |
548 | if tag.has_attr('tags'):
549 | self.tags = tag['tags']
550 |
551 | if tag.has_attr('path'):
552 | self.path = tag['path']
553 |
554 | if tag.has_attr('attr'):
555 | self.attr = tag['attr']
556 |
557 | for text in tag.find_all('text'):
558 | t = Text()
559 | t.From_SVG(text, rotate)
560 | self.fp_text.append(t)
561 |
562 | if tag.has_attr('model'):
563 | modeltag = tag['model']
564 | model = []
565 | model.append(modeltag[0:modeltag.find(';')])
566 | modeltag = modeltag[modeltag.find(';') + 1:]
567 | offset = ['xyz'] + modeltag[0:modeltag.find(';')].split(',')
568 | modeltag = modeltag[modeltag.find(';') + 1:]
569 | scale = ['xyz'] + modeltag[0:modeltag.find(';')].split(',')
570 | modeltag = modeltag[modeltag.find(';') + 1:]
571 | modelrotate = ['xyz'] + modeltag[0:modeltag.find(';')].split(',')
572 | model.append(['offset', offset])
573 | model.append(['scale', scale])
574 | model.append(['rotate', modelrotate])
575 | self.model = model
576 |
577 | for tagpath in tag.find_all('path'):
578 | # if path.has_attr('type') == True and path['type'] == 'zone':
579 | # self.zone.append(Zone.From_Svg(path))
580 | # print(tag)
581 | if tagpath.has_attr('type') == True and tagpath['type'] == 'fp_poly':
582 | poly = Poly()
583 | poly.From_SVG(tagpath)
584 | self.fp_poly.insert(0, poly)
585 | elif tagpath.has_attr('type') == True and tagpath['type'] == 'fp_line':
586 | paths = parse_path(tagpath['d'])
587 |
588 | for path in paths:
589 | line = Line()
590 | line.From_SVG(tagpath, path)
591 | self.fp_line.insert(0, line)
592 | elif tagpath.has_attr('type') == True and tagpath['type'] == 'fp_arc':
593 | paths = parse_path(tagpath['d'])
594 |
595 | for path in paths:
596 | arc = Arc()
597 | arc.From_SVG(tagpath, path)
598 | self.fp_arc.insert(0, arc)
599 |
600 | elif tagpath.has_attr('type') == True and tagpath['type'] == 'fp_curve':
601 | curve = Curve()
602 | curve.From_SVG(path)
603 | self.fp_curve.insert(0, curve)
604 | # else:
605 | # segment, gr_line, gr_arc, gr_curve = self.Parse_Segment(path)
606 | # segments = segments + segment
607 | # gr_line[0][0] = 'fp_line'
608 | # gr_lines += gr_line
609 | # gr_arcs += gr_arc
610 | # gr_curves += gr_curve
611 | for tagpath in tag.find_all('rect'):
612 | if tagpath.has_attr('type') == True and tagpath['type'] == 'pad':
613 | pad = Pad()
614 | pad.From_SVG(tagpath, rotate)
615 | self.pad.append(pad)
616 |
617 | for tagpath in tag.find_all('circle'):
618 | if tagpath.has_attr('type') == True and tagpath['type'] == 'pad':
619 | pad = Pad()
620 | pad.From_SVG(tagpath, rotate)
621 | self.pad.append(pad)
622 | elif tagpath.has_attr('type') == True and tagpath['type'] == 'fp_circle':
623 | circle = Circle()
624 | circle.From_SVG(tagpath)
625 | self.fp_circle.append(circle)
626 |
627 | for tagpath in tag.find_all('ellipse'):
628 | if tagpath.has_attr('type') == True and tagpath['type'] == 'pad':
629 | pad = Pad()
630 | pad.From_SVG(tagpath, rotate)
631 | self.pad.append(pad)
632 |
--------------------------------------------------------------------------------
/Stretch/kiplug/pad.py:
--------------------------------------------------------------------------------
1 | from .colour import Colour
2 |
3 | # https://github.com/KiCad/kicad-source-mirror/blob/93466fa1653191104c5e13231dfdc1640b272777/pcbnew/plugins/kicad/pcb_parser.cpp#L3530
4 | # 0 pad
5 | # 1 1/2/3
6 | # 2 smd
7 | # 3 rect
8 | # 4
9 | # 0 at
10 | # 1 66.66
11 | # 2 99.99
12 | # 2 180
13 | # 5
14 | # 0 size
15 | # 1 0.9
16 | # 2 1.2
17 | # 6
18 | # 0 layers
19 | # 1 F.Cu
20 | # 2 F.Paste
21 | # 3 F.Mask
22 | # 7
23 | # 0 net
24 | # 1 16
25 | # 2 Net-(D4-Pad1)
26 | # 8
27 | # 0 pinfunction
28 | # 1 "A"
29 | # 9
30 | # 0 pintype
31 | # 1 "passive"
32 |
33 |
34 | pxToMM = 96 / 25.4
35 |
36 | class Pad(object):
37 |
38 | def __init__(self):
39 | self.name = ''
40 | #thru_hole, smd, connect, or np_thru_hole
41 | self.attribute = ''
42 | self.shape = ''
43 | self.size = []
44 | self.at = []
45 | self.rect_delta = []
46 | self.drill = []
47 | self.layers = []
48 | self.net = []
49 | self.pinfunction = ''
50 | self.pintype = ''
51 | self.die_length = ''
52 | self.solder_mask_margin = ''
53 | self.solder_paste_margin = ''
54 | self.solder_paste_margin_ratio = ''
55 | self.clearance = ''
56 | self.zone_connect = ''
57 | self.thermal_width = ''
58 | self.thermal_gap = ''
59 | self.roundrect_rratio = ''
60 | self.chamfer_ratio = ''
61 | self.chamfer = ''
62 | self.property = ''
63 | self.options = ''
64 | self.primitives = ''
65 | self.remove_unused_layers = ''
66 | self.keep_end_layers = ''
67 | self.tstamp = ''
68 |
69 | def From_PCB(self, input):
70 |
71 | at = []
72 | size = []
73 | layers = []
74 | roundrect_rratio = ''
75 | net = ''
76 | rotate = ''
77 |
78 | if input[0] != 'pad':
79 | assert False,"Pad: Not a pad"
80 | return None
81 |
82 | self.name = input[1]
83 |
84 | self.attribute = input[2]
85 |
86 | self.shape = input[3]
87 |
88 | for item in input[4:]:
89 | if item[0] == 'size':
90 | self.size = item[1:]
91 |
92 | if item[0] == 'at':
93 | for at in item[1:]:
94 | self.at.append(float(at))
95 |
96 | #Todo: proper handling for many of these
97 | if item[0] == 'rect_delta':
98 | self.rect_delta = item[1]
99 | if item[0] == 'drill':
100 | self.drill = item[1]
101 | if item[0] == 'layers':
102 | for layer in item[1:]:
103 | self.layers.append(layer)
104 | if item[0] == 'net':
105 | self.net = item[1:]
106 | if item[0] == 'pinfunction':
107 | self.pinfunction = item[1]
108 | if item[0] == 'pintype':
109 | self.pintype = item[1]
110 | if item[0] == 'die_length':
111 | self.die_length = item[1]
112 | if item[0] == 'solder_mask_margin':
113 | self.solder_mask_margin = item[1]
114 | if item[0] == 'solder_paste_margin':
115 | self.solder_paste_margin = item[1]
116 | if item[0] == 'solder_paste_margin_ratio':
117 | self.solder_paste_margin_ratio = item[1]
118 | if item[0] == 'clearance':
119 | self.clearance = item[1]
120 | if item[0] == 'zone_connect':
121 | self.zone_connect = item[1]
122 | if item[0] == 'thermal_width':
123 | self.thermal_width = item[1]
124 | if item[0] == 'thermal_gap':
125 | self.thermal_gap = item[1]
126 | if item[0] == 'roundrect_rratio':
127 | self.roundrect_rratio = item[1]
128 | if item[0] == 'chamfer_ratio':
129 | self.chamfer_ratio = item[1]
130 | if item[0] == 'chamfer':
131 | self.chamfer = item[1]
132 | if item[0] == 'property':
133 | self.property = item[1]
134 | if item[0] == 'options':
135 | self.options = item[1]
136 | if item[0] == 'primitives':
137 | self.primitives = item[1]
138 | if item[0] == 'remove_unused_layers':
139 | self.remove_unused_layers = item[1]
140 | if item[0] == 'keep_end_layers':
141 | self.keep_end_layers = item[1]
142 | if item[0] == 'tstamp':
143 | self.tstamp = item[1]
144 |
145 | def To_PCB(self):
146 | pcb = ['pad']
147 |
148 | pcb.append(self.name)
149 | pcb.append(self.attribute)
150 | pcb.append(self.shape)
151 |
152 | pcb.append(['at'] + self.at)
153 | pcb.append(['size'] + self.size)
154 | if len(self.drill) > 0:
155 | pcb.append(['drill', self.drill])
156 | pcb.append(['layers'] + self.layers)
157 | if len(self.net) > 0:
158 | pcb.append(['net'] + self.net)
159 | if len(self.pinfunction) > 0:
160 | pcb.append(['pinfunction', self.pinfunction])
161 | if len(self.pintype) > 0:
162 | pcb.append(['pintype', self.pintype])
163 |
164 | if self.tstamp:
165 | pcb.append(['tstamp', self.tstamp])
166 |
167 | return pcb
168 |
169 |
170 |
171 | def To_SVG(self, angle = 0.0):
172 | angle = float(angle)
173 |
174 | layers = []
175 | roundrect_rratio = ''
176 | net = ''
177 | drill = ''
178 | rotate = ''
179 | parameters = ''
180 |
181 | # Corner coordinates to centre coordinate system
182 | x = self.at[0] - float(self.size[0]) / 2
183 | y = self.at[1] - float(self.size[1]) / 2
184 |
185 | if len(self.at) > 2:
186 | angle += float(self.at[2])
187 | if angle != 0.0:
188 | rotate += 'transform="rotate(' + str(angle) + ', ' + str(self.at[0] * pxToMM) + ', ' + str(self.at[1] * pxToMM) + ')" '
189 |
190 | if self.roundrect_rratio != '':
191 | roundrect_rratio = 'roundrect_rratio="' + self.roundrect_rratio + '" '
192 |
193 | if self.drill:
194 | drill = 'drill="' + self.drill + '" '
195 |
196 | pintype = ''
197 | if self.pintype:
198 | pintype = 'pintype="' + self.pintype + '" '
199 |
200 | pinfunction = ''
201 | if self.pinfunction:
202 | pinfunction = 'pinfunction="' + self.pinfunction + '" '
203 |
204 | if len(self.net) > 1:
205 | net = 'netid="' + self.net[0] + '" '
206 | net += 'netname="' + self.net[1] + '" '
207 |
208 | svg = ''
209 | svgsize = ''
210 | roundcorners = ''
211 |
212 | if self.shape == 'rect':
213 | parameters += ''
273 |
274 | svg += parameters
275 | # print(parameters)
276 | return svg
277 |
278 |
279 | def From_SVG(self, tag, angle = 0):
280 |
281 | self.name = tag['name']
282 | self.attribute = tag['attribute']
283 |
284 | if tag.has_attr('shape'):
285 | self.shape = tag['shape']
286 |
287 | if self.shape == 'rect' or self.shape == 'roundrect':
288 |
289 | width = float(tag['width']) / pxToMM
290 | height = float(tag['height']) / pxToMM
291 | x = str((float(tag['x']) / pxToMM) + (width / 2))
292 | y = str((float(tag['y']) / pxToMM) + (height / 2))
293 | width = str(width)
294 | height = str(height)
295 |
296 | self.size = [width, height]
297 |
298 | elif self.shape == 'circle':
299 | r = str((float(tag['r']) * 2) / pxToMM)
300 | self.size = [r, r]
301 | x = str(float(tag['cx']) / pxToMM)
302 | y = str(float(tag['cy']) / pxToMM)
303 |
304 | elif self.shape == 'oval':
305 | if tag.has_attr('rx'):
306 | rx = str((float(tag['rx']) * 2) / pxToMM)
307 | ry = str((float(tag['ry']) * 2) / pxToMM)
308 | self.size = [rx, ry]
309 | elif tag.has_attr('r'):
310 | r = str((float(tag['r']) * 2) / pxToMM)
311 | self.size = [r, r]
312 |
313 | x = str(float(tag['cx']) / pxToMM)
314 | y = str(float(tag['cy']) / pxToMM)
315 | else:
316 | print(self.shape)
317 |
318 |
319 | self.at = [x, y]
320 | if tag.has_attr('transform'):
321 | transform = tag['transform']
322 | if 'rotate(' in transform:
323 | rotate = transform[transform.find('rotate(') + 7:]
324 | rotate = rotate[:rotate.find(')')]
325 | if ',' in rotate:
326 | rotate = rotate[:rotate.find(',')]
327 | angle += (float(rotate) * -1) + 180
328 |
329 | if angle != 0:
330 | self.at.append(str(angle))
331 |
332 |
333 | if tag.has_attr('drill'):
334 | self.drill = tag['drill']
335 |
336 | if tag.has_attr('pintype'):
337 | self.pintype = tag['pintype']
338 |
339 | if tag.has_attr('pinfunction'):
340 | self.pinfunction = tag['pinfunction']
341 |
342 | self.layers = tag['layers'].split(',')
343 |
344 | if tag.has_attr('roundrect_rratio'):
345 | self.roundrect_rratio = tag['roundrect_rratio']
346 | self.shape = 'roundrect'
347 |
348 | if tag.has_attr('netid'):
349 | self.net = [tag['netid'], tag['netname']]
350 |
351 |
352 |
353 |
354 |
355 |
--------------------------------------------------------------------------------
/Stretch/kiplug/parser_base.py:
--------------------------------------------------------------------------------
1 | """
2 | From the excellent Interactive HTML BOM project
3 | https://github.com/openscopeproject/InteractiveHtmlBom
4 |
5 | MIT License
6 |
7 | Copyright (c) 2018 qu1ck
8 |
9 | Permission is hereby granted, free of charge, to any person obtaining a copy
10 | of this software and associated documentation files (the "Software"), to deal
11 | in the Software without restriction, including without limitation the rights
12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 | copies of the Software, and to permit persons to whom the Software is
14 | furnished to do so, subject to the following conditions:
15 |
16 | The above copyright notice and this permission notice shall be included in all
17 | copies or substantial portions of the Software.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | SOFTWARE.
26 |
27 | """
28 |
29 |
30 | class ParserBase:
31 | DEFAULT_FIELDS = []
32 |
33 | def __init__(self, file_name = None):
34 | """
35 | :param file_name: path to file that should be parsed.
36 | """
37 | self.file_name = file_name
38 |
39 | @staticmethod
40 | def normalize_field_names(data):
41 | field_map = {f.lower(): f for f in reversed(data[0])}
42 |
43 | def remap(ref_fields):
44 | return {field_map[f.lower()]: v for (f, v) in
45 | sorted(ref_fields.items(), reverse=True)}
46 |
47 | field_data = {r: remap(d) for (r, d) in data[1].items()}
48 | return field_map.values(), field_data
49 |
50 | def parse(self, normalize_case):
51 | data = self.get_extra_field_data()
52 | if data is None:
53 | return None
54 | if normalize_case:
55 | data = self.normalize_field_names(data)
56 | return sorted(data[0]), data[1]
57 |
58 | def get_extra_field_data(self):
59 | # type: () -> tuple
60 | """
61 | Parses the file and returns a extra field data.
62 | :return: tuple of the format
63 | (
64 | [field_name1, field_name2,... ],
65 | {
66 | ref1: {
67 | field_name1: field_value1,
68 | field_name2: field_value2,
69 | ...
70 | ],
71 | ref2: ...
72 | }
73 | )
74 | """
75 | pass
76 |
77 | def parse_sexpression(self, sexpression):
78 | import re
79 |
80 | term_regex = r'''(?mx)
81 | \s*(?:
82 | (?P\()|
83 | (?P\))|
84 | (?P"(?:\\\\|\\"|[^"])*")|
85 | (?P[^(^)\s]+)
86 | )'''
87 | pattern = re.compile(term_regex)
88 | stack = []
89 | out = []
90 | for terms in pattern.finditer(sexpression):
91 | term, value = [(t, v) for t, v in terms.groupdict().items() if v][0]
92 | if term == 'open':
93 | stack.append(out)
94 | out = []
95 | elif term == 'close':
96 | assert stack, "Trouble with nesting of brackets"
97 | tmp, out = out, stack.pop(-1)
98 | out.append(tmp)
99 | elif term == 'sq':
100 | out.append(value[1:-1].replace('\\\\', '\\').replace('\\"', '"'))
101 | elif term == 's':
102 | out.append(value)
103 | else:
104 | raise NotImplementedError("Error: %s, %s" % (term, value))
105 | assert not stack, "Trouble with nesting of brackets"
106 | return out[0]
107 |
108 |
--------------------------------------------------------------------------------
/Stretch/kiplug/pcb_writer.py:
--------------------------------------------------------------------------------
1 | import io, os
2 | import sys
3 |
4 | from bs4 import BeautifulSoup
5 |
6 |
7 | import json
8 | import re
9 | import math
10 | import cmath
11 |
12 | #Running KiCad Linux vs. standalone requires different imports
13 | try:
14 | # from .stretch import Board
15 | from .sexpressions_writer import SexpressionWriter
16 | except:
17 | # from stretch import Board
18 | from sexpressions_writer import SexpressionWriter
19 |
20 | pxToMM = 3.779528
21 |
22 | class PcbWrite(object):
23 | def __init__(self):
24 | currentdir = os.path.dirname(os.path.realpath(__file__))
25 | self.filename_in = os.path.join(currentdir, 'tests', 'out.svg')
26 | self.filename_out = os.path.join(currentdir, 'tests', 'out.kicad_pcb')
27 | self.filename_json = os.path.join(currentdir, 'tests', 'out.json')
28 |
29 | def Load(self, filename = None):
30 | if filename is None:
31 | filename = self.filename_in
32 |
33 | with open(filename, "r") as f:
34 |
35 | contents = f.read()
36 | svg = BeautifulSoup(contents, 'html.parser')
37 | return svg
38 |
39 | def Save(self, lst, filename = None):
40 | if filename is None:
41 | filename = self.filename_json
42 |
43 | with open(filename, 'w') as f:
44 | f.write(lst)
45 |
46 | def Save_Json(self, obj, save = False):
47 | currentdir = os.path.dirname(os.path.realpath(__file__))
48 | self.filename_json = os.path.join(currentdir, 'example', 'out.json')
49 | js = json.dumps(obj)
50 | if save:
51 | with open(self.filename_json, 'wb') as f:
52 | f.write(js)
53 | return js
54 |
55 |
56 | def Run_Standalone(self):
57 | svg = self.Load()
58 | # lst = self.Svg_To_List(svg)
59 |
60 | board = Board()
61 | board.From_SVG(svg)
62 |
63 | lst = board.To_PCB()
64 | # print(lst)
65 |
66 | a = SexpressionWriter()
67 |
68 | sexpression = a.List_To_Sexpression(lst)
69 | a.Save(sexpression)
70 | # self.Save(sexpression)
71 |
72 | def Run_Plugin(self, pcb_filename, svg_filename):
73 | from .board import Board
74 |
75 | infile = os.path.join(os.path.dirname(pcb_filename),svg_filename)
76 |
77 | svg = self.Load(infile)
78 |
79 | board = Board()
80 | board.From_SVG(svg)
81 |
82 | lst = board.To_PCB()
83 |
84 | a = SexpressionWriter()
85 |
86 | sexpression = a.List_To_Sexpression(lst)
87 | a.Save(sexpression, pcb_filename)
88 |
89 |
90 | if __name__ == '__main__':
91 | e = PcbWrite()
92 | e.Run_Standalone()
--------------------------------------------------------------------------------
/Stretch/kiplug/poly.py:
--------------------------------------------------------------------------------
1 |
2 | from .colour import Colour
3 | from .svgpath import parse_path
4 |
5 | # 0 gr_poly
6 | # 1
7 | # 0 pts
8 | # 1
9 | # 0 xy
10 | # 1 147.6375
11 | # 2 120.9675
12 | # 2
13 | # 0 xy
14 | # 1 147.6375
15 | # 2 120.9675
16 | # 3
17 | # ...
18 | # 2
19 | # 0 layer
20 | # 1 B.Cu
21 | # 3
22 | # 0 width
23 | # 1 0.1
24 |
25 | pxToMM = 96 / 25.4
26 |
27 | class Poly(object):
28 |
29 | def __init__(self):
30 | self.pts = []
31 | self.width = 0
32 | self.layer = ''
33 | self.fill = ''
34 | self.tstamp = ''
35 | self.status = 0
36 |
37 | def From_PCB(self, input):
38 | for item in input:
39 | if item[0] == 'pts':
40 | for xy in item:
41 | if xy[0] == 'xy':
42 | self.pts.append([xy[1], xy[2]])
43 |
44 | if item[0] == 'layer':
45 | self.layer = item[1]
46 |
47 | if item[0] == 'width':
48 | self.width = item[1]
49 |
50 | if item[0] == 'fill':
51 | self.fill = item[1]
52 |
53 | if item[0] == 'tstamp':
54 | self.tstamp = item[1]
55 |
56 | if item[0] == 'status':
57 | self.status = item[1]
58 |
59 | def To_PCB(self, fp = False):
60 | pcb = []
61 | if fp:
62 | pcb = ['fp_poly']
63 | else:
64 | pcb = ['gr_poly']
65 |
66 | pts = ['pts']
67 |
68 | for item in self.pts:
69 | xy = ['xy'] + item
70 | pts += [xy]
71 |
72 | pcb.append(pts)
73 | if self.width != 0:
74 | pcb.append(['width', self.width])
75 | pcb.append(['layer', self.layer])
76 | if self.tstamp != '':
77 | pcb.append(['fill', self.fill])
78 | if self.tstamp != '':
79 | pcb.append(['tstamp', self.tstamp])
80 | if self.status != 0:
81 | pcb.append(['status', self.status])
82 |
83 | # print(pcb)
84 |
85 | return pcb
86 |
87 | def To_SVG(self, fp = False):
88 | if fp:
89 | polytype = 'fp_poly'
90 | else:
91 | polytype = 'gr_poly'
92 |
93 | xy_text = ''
94 |
95 | for xy in self.pts:
96 | xy_text += ' ' + str(float(xy[0]) * pxToMM)
97 | xy_text += ',' + str(float(xy[1]) * pxToMM)
98 |
99 | parameters = ''
107 |
108 | # print(parameters)
109 | return parameters
110 |
111 |
112 | def From_SVG(self, tag):
113 | data = [tag['type']]
114 | style = tag['style']
115 |
116 | styletag = style[style.find('stroke-width:') + 13:]
117 | width = styletag[0:styletag.find('mm')]
118 |
119 | if tag.has_attr('layer'):
120 | layer = tag['layer']
121 | elif tag.parent.has_attr('inkscape:label'):
122 | #XML metadata trashed, try to recover from parent tag
123 | layer = tag.parent['inkscape:label']
124 | else:
125 | assert False, "Poly not in layer"
126 |
127 | path = parse_path(tag['d'])
128 |
129 | pts = []
130 | for point in path:
131 | xy = []
132 | xy.append(str(point.start.real / pxToMM))
133 | xy.append(str(point.start.imag / pxToMM))
134 | pts.append(xy)
135 |
136 | xy = []
137 | xy.append(str(path[0].start.real / pxToMM))
138 | xy.append(str(path[0].start.imag / pxToMM))
139 | pts.append(xy)
140 |
141 | self.pts = pts
142 | self.width = width
143 | self.layer = layer
144 |
--------------------------------------------------------------------------------
/Stretch/kiplug/segment.py:
--------------------------------------------------------------------------------
1 |
2 | from .colour import Colour
3 |
4 |
5 | #https://github.com/KiCad/kicad-source-mirror/blob/93466fa1653191104c5e13231dfdc1640b272777/pcbnew/plugins/kicad/pcb_parser.cpp#L4204
6 |
7 | # 0 segment
8 | # 1
9 | # 0 start
10 | # 1 66.66
11 | # 2 99.99
12 | # 2
13 | # 0 end
14 | # 1 66.66
15 | # 2 99.99
16 | # 3
17 | # 0 width
18 | # 1 0.25
19 | # 4
20 | # 0 layer
21 | # 1 B.Cu
22 | # 5
23 | # 0 net
24 | # 1 1
25 |
26 |
27 | pxToMM = 96 / 25.4
28 |
29 | class Segment(object):
30 |
31 | def __init__(self):
32 | self.start = []
33 | self.end = []
34 | self.width = 0
35 | self.layer = ''
36 | self.net = 0
37 | self.tstamp = ''
38 | self.status = 0
39 | self.locked = False
40 |
41 | def From_SVG(self, tag, path):
42 | style = tag['style']
43 |
44 | width = style[style.find('stroke-width:') + 13:]
45 | self.width = width[0:width.find('mm')]
46 |
47 | if tag.has_attr('layer'):
48 | self.layer = tag['layer']
49 | elif tag.parent.has_attr('inkscape:label'):
50 | #XML metadata trashed, try to recover from parent tag
51 | self.layer = tag.parent['inkscape:label']
52 | else:
53 | assert False, "Path not in layer"
54 |
55 |
56 | self.start = [str(path.start.real / pxToMM), str(path.start.imag / pxToMM)]
57 | self.end = [str(path.end.real / pxToMM), str(path.end.imag / pxToMM)]
58 |
59 | if tag.has_attr('net'):
60 | self.net = tag['net']
61 |
62 | if tag.has_attr('status'):
63 | self.status = tag['status']
64 |
65 | if tag.has_attr('tstamp'):
66 | self.tstamp = tag['tstamp']
67 |
68 | if tag.has_attr('locked'):
69 | self.locked = True
70 |
71 |
72 | def To_PCB(self):
73 | pcb = ['segment']
74 |
75 | pcb.append(['start'] + self.start)
76 | pcb.append(['end'] + self.end)
77 | pcb.append(['width', self.width])
78 | pcb.append(['layer', self.layer])
79 | pcb.append(['net', self.net])
80 | if self.tstamp:
81 | pcb.append(['tstamp', self.tstamp])
82 | pcb.append(['status', self.status])
83 | if self.locked:
84 | pcb.append(['locked'])
85 |
86 | return pcb
87 |
88 | def From_PCB(self, pcblist):
89 | if pcblist[0] != 'segment':
90 | assert False,"Segment: Not a segment"
91 | return None
92 |
93 | for item in pcblist:
94 |
95 | if item[0] == 'start':
96 | self.start = [item[1], item[2]]
97 |
98 | if item[0] == 'end':
99 | self.end = [item[1], item[2]]
100 |
101 | if item[0] == 'width':
102 | self.width = item[1]
103 |
104 | if item[0] == 'layer':
105 | self.layer = item[1]
106 |
107 | if item[0] == 'net':
108 | self.net = item[1]
109 |
110 | if item[0] == 'tstamp':
111 | self.tstamp = item[1]
112 |
113 | if item[0] == 'status':
114 | self.status = item[1]
115 |
116 |
117 | def To_SVG(self):
118 | tstamp = ''
119 | status = ''
120 |
121 | if self.tstamp != '':
122 | tstamp = 'tstamp="' + self.tstamp + '" '
123 | if self.status != '':
124 | status = 'status="' + str(self.status) + '" '
125 |
126 | parameters = ''
138 |
139 | # print(parameters)
140 | return parameters
--------------------------------------------------------------------------------
/Stretch/kiplug/sexpressions_writer.py:
--------------------------------------------------------------------------------
1 | import io, os
2 | import sys
3 |
4 | from bs4 import BeautifulSoup
5 | import json
6 | import re
7 |
8 | #Running KiCad Linux vs. standalone requires different imports
9 | try:
10 | from .parser_base import ParserBase
11 | # from .sexpressions_parser import parse_sexpression
12 | except:
13 | from parser_base import ParserBase
14 | # from sexpressions_parser import parse_sexpression
15 |
16 |
17 | class SexpressionWriter(object):
18 | def __init__(self):
19 | self.filename_in = os.path.join('tests', 'out.svg')
20 | self.filename_out = os.path.join('tests', 'out.kicad_pcb')
21 | self.filename_sexpression = os.path.join('tests', 'complex.kicad_pcb')
22 |
23 | def Load(self):
24 | with open(self.filename_in, "r") as f:
25 |
26 | contents = f.read()
27 | svg = BeautifulSoup(contents, 'html.parser')
28 | return svg
29 |
30 | def Load_Sexpression(self):
31 | with io.open(self.filename_sexpression, 'r', encoding='utf-8') as f:
32 | sexpression = ParserBase.parse_sexpression(f.read())
33 | return sexpression
34 |
35 |
36 |
37 | def List_Escape(self, lst):
38 | newlist = []
39 | RE = re.compile('[\\\/\?%\(\)\ ]|^$')
40 |
41 | for value in lst:
42 | if type(value) is not list:
43 | # print(value)
44 | if RE.search(value):
45 | value = '"' + value + '"'
46 |
47 | newlist.append(value)
48 |
49 | return newlist
50 |
51 |
52 | def List_To_Sexpression(self, lst, first = True):
53 | line = []
54 |
55 | lst = self.List_Escape(lst)
56 |
57 | for value in lst:
58 | if type(value) is list:
59 | line.append('(' + self.List_To_Sexpression(value, False) + ')\n')
60 | else:
61 | line.append(value)
62 |
63 | out = ' '.join(line)
64 |
65 | if first:
66 | out = '(' + out + ')'
67 |
68 | return out
69 |
70 | def Save(self, sexpression, filename = None):
71 | if filename is None:
72 | filename = self.filename_out
73 |
74 | with open(filename, 'w') as f:
75 | f.write(sexpression)
76 |
77 | def Run(self):
78 | # svg = self.Load()
79 | lst = self.Load_Sexpression()
80 | sexpression = self.List_To_Sexpression(lst)
81 | self.Save(sexpression)
82 |
83 |
84 | if __name__ == '__main__':
85 | e = SexpressionWriter()
86 | e.Run()
--------------------------------------------------------------------------------
/Stretch/kiplug/svg_writer.py:
--------------------------------------------------------------------------------
1 | import io, os
2 | import sys
3 |
4 | from bs4 import BeautifulSoup
5 |
6 |
7 | import json
8 | import math
9 | import cmath
10 |
11 | from datetime import datetime
12 |
13 | #Running KiCad Linux vs. standalone requires different imports
14 | # try:
15 | # # from .stretch import Board
16 | # from .parser_base import ParserBase
17 | # from .sexpressions_parser import parse_sexpression
18 | # from .sexpressions_writer import SexpressionWriter
19 | # except:
20 | # # from stretch import Board
21 | # from parser_base import ParserBase
22 | # from sexpressions_parser import parse_sexpression
23 | # from sexpressions_writer import SexpressionWriter
24 |
25 |
26 | # import stretch
27 | # from .board import Board
28 |
29 | # try:
30 | # from .board import Board
31 | # except:
32 | # from board import Board
33 |
34 |
35 | class SvgWrite(object):
36 | def __init__(self):
37 | print(os.path.dirname(os.path.realpath(__file__)) )
38 | currentdir = os.path.dirname(os.path.realpath(__file__))
39 | testdir = os.path.dirname(os.path.dirname(currentdir))
40 | self.filename_in = os.path.join(currentdir, 'tests', 'complex.kicad_pcb')
41 | # self.filename_in = os.path.join(currentdir, 'tests', 'simple.kicad_pcb')
42 | self.filename_json = os.path.join(testdir, 'tests', 'out.json')
43 | self.filename_svg = os.path.join(currentdir, 'tests', 'out.svg')
44 | self.filename_base = os.path.join(currentdir, 'tests', 'base.svg')
45 |
46 | self.hiddenLayers = []
47 |
48 |
49 | def Load(self, filename = None):
50 |
51 | # try:
52 | # from .sexpressions_parser import parse_sexpression
53 | # except:
54 | # from sexpressions_parser import parse_sexpression
55 | from .parser_base import ParserBase
56 |
57 | if filename is None:
58 | filename = self.filename_in
59 |
60 | with io.open(filename, 'r', encoding='utf-8') as f:
61 | sexpression = ParserBase().parse_sexpression(f.read())
62 | return sexpression
63 |
64 | def Convert(self, obj, save = False):
65 | js = json.dumps(obj)
66 | if save:
67 | with open(self.filename_json, 'w') as f:
68 | f.write(js)
69 | return js
70 |
71 | def Save(self, svg, filename = None):
72 | if filename is None:
73 | filename = self.filename_svg
74 |
75 | # dt = datetime.now()
76 | # ts = datetime.timestamp(dt)
77 | # timestamp = str(int(ts))
78 |
79 | # filename = filename + timestamp
80 |
81 | with open(filename, 'wb') as f:
82 | f.write(svg)
83 |
84 | def Print_Headings(self, dic):
85 | for item in dic:
86 | if type(item) is str:
87 | print(item)
88 | else:
89 | print(item[0])
90 |
91 | def Run_Standalone(self):
92 | # dic = self.Load()
93 |
94 | #Save JSON file, for development
95 | #self.Convert(dic, True)
96 |
97 | # with open(self.filename_base, "r") as f:
98 | # contents = f.read()
99 | # base = BeautifulSoup(contents, 'html.parser')
100 |
101 |
102 | board = Board()
103 | board.clear()
104 | board.From_PCB(dic)
105 |
106 | svg = board.To_SVG()
107 |
108 | self.Save(svg)
109 |
110 |
111 | def Run_Plugin(self, filename, outfilename):
112 | from .board import Board
113 | dic = self.Load(filename)
114 | # dic = self.Convert(dic, True)
115 |
116 | outfile = os.path.join(os.path.dirname(filename), outfilename)
117 |
118 | board = Board()
119 | board.From_PCB(dic)
120 |
121 | svg = board.To_SVG()
122 |
123 | self.Save(svg, outfile)
124 |
125 |
126 | if __name__ == '__main__':
127 | e = SvgWrite()
128 | e.Run_Standalone()
--------------------------------------------------------------------------------
/Stretch/kiplug/svgpath.py:
--------------------------------------------------------------------------------
1 | """This submodule contains very stripped down bare bones version of
2 | svgpathtools module:
3 | https://github.com/mathandy/svgpathtools
4 |
5 | All external dependencies are removed. This code can parse path strings with
6 | segments and arcs, calculate bounding box and that's about it. This is all
7 | that is needed in ibom parsers at the moment.
8 | """
9 |
10 | # External dependencies
11 | from __future__ import division, absolute_import, print_function
12 |
13 | import re
14 | from cmath import exp
15 | from math import sqrt, cos, sin, acos, degrees, radians, pi
16 |
17 |
18 | def clip(a, a_min, a_max):
19 | return min(a_max, max(a_min, a))
20 |
21 |
22 | class Line(object):
23 | def __init__(self, start, end):
24 | self.start = start
25 | self.end = end
26 |
27 | def __repr__(self):
28 | return 'Line(start=%s, end=%s)' % (self.start, self.end)
29 |
30 | def __eq__(self, other):
31 | if not isinstance(other, Line):
32 | return NotImplemented
33 | return self.start == other.start and self.end == other.end
34 |
35 | def __ne__(self, other):
36 | if not isinstance(other, Line):
37 | return NotImplemented
38 | return not self == other
39 |
40 | def __len__(self):
41 | return 2
42 |
43 | def bbox(self):
44 | """returns the bounding box for the segment in the form
45 | (xmin, xmax, ymin, ymax)."""
46 | xmin = min(self.start.real, self.end.real)
47 | xmax = max(self.start.real, self.end.real)
48 | ymin = min(self.start.imag, self.end.imag)
49 | ymax = max(self.start.imag, self.end.imag)
50 | return xmin, xmax, ymin, ymax
51 |
52 |
53 | class Arc(object):
54 | def __init__(self, start, radius, rotation, large_arc, sweep, end,
55 | autoscale_radius=True):
56 | """
57 | This should be thought of as a part of an ellipse connecting two
58 | points on that ellipse, start and end.
59 | Parameters
60 | ----------
61 | start : complex
62 | The start point of the curve. Note: `start` and `end` cannot be the
63 | same. To make a full ellipse or circle, use two `Arc` objects.
64 | radius : complex
65 | rx + 1j*ry, where rx and ry are the radii of the ellipse (also
66 | known as its semi-major and semi-minor axes, or vice-versa or if
67 | rx < ry).
68 | Note: If rx = 0 or ry = 0 then this arc is treated as a
69 | straight line segment joining the endpoints.
70 | Note: If rx or ry has a negative sign, the sign is dropped; the
71 | absolute value is used instead.
72 | Note: If no such ellipse exists, the radius will be scaled so
73 | that one does (unless autoscale_radius is set to False).
74 | rotation : float
75 | This is the CCW angle (in degrees) from the positive x-axis of the
76 | current coordinate system to the x-axis of the ellipse.
77 | large_arc : bool
78 | Given two points on an ellipse, there are two elliptical arcs
79 | connecting those points, the first going the short way around the
80 | ellipse, and the second going the long way around the ellipse. If
81 | `large_arc == False`, the shorter elliptical arc will be used. If
82 | `large_arc == True`, then longer elliptical will be used.
83 | In other words, `large_arc` should be 0 for arcs spanning less than
84 | or equal to 180 degrees and 1 for arcs spanning greater than 180
85 | degrees.
86 | sweep : bool
87 | For any acceptable parameters `start`, `end`, `rotation`, and
88 | `radius`, there are two ellipses with the given major and minor
89 | axes (radii) which connect `start` and `end`. One which connects
90 | them in a CCW fashion and one which connected them in a CW
91 | fashion. If `sweep == True`, the CCW ellipse will be used. If
92 | `sweep == False`, the CW ellipse will be used. See note on curve
93 | orientation below.
94 | end : complex
95 | The end point of the curve. Note: `start` and `end` cannot be the
96 | same. To make a full ellipse or circle, use two `Arc` objects.
97 | autoscale_radius : bool
98 | If `autoscale_radius == True`, then will also scale `self.radius`
99 | in the case that no ellipse exists with the input parameters
100 | (see inline comments for further explanation).
101 |
102 | Derived Parameters/Attributes
103 | -----------------------------
104 | self.theta : float
105 | This is the phase (in degrees) of self.u1transform(self.start).
106 | It is $\\theta_1$ in the official documentation and ranges from
107 | -180 to 180.
108 | self.delta : float
109 | This is the angular distance (in degrees) between the start and
110 | end of the arc after the arc has been sent to the unit circle
111 | through self.u1transform().
112 | It is $\\Delta\\theta$ in the official documentation and ranges from
113 | -360 to 360; being positive when the arc travels CCW and negative
114 | otherwise (i.e. is positive/negative when sweep == True/False).
115 | self.center : complex
116 | This is the center of the arc's ellipse.
117 | self.phi : float
118 | The arc's rotation in radians, i.e. `radians(self.rotation)`.
119 | self.rot_matrix : complex
120 | Equal to `exp(1j * self.phi)` which is also equal to
121 | `cos(self.phi) + 1j*sin(self.phi)`.
122 |
123 |
124 | Note on curve orientation (CW vs CCW)
125 | -------------------------------------
126 | The notions of clockwise (CW) and counter-clockwise (CCW) are reversed
127 | in some sense when viewing SVGs (as the y coordinate starts at the top
128 | of the image and increases towards the bottom).
129 | """
130 | assert start != end
131 | assert radius.real != 0 and radius.imag != 0
132 |
133 | self.start = start
134 | self.radius = abs(radius.real) + 1j * abs(radius.imag)
135 | self.rotation = rotation
136 | self.large_arc = bool(large_arc)
137 | self.sweep = bool(sweep)
138 | self.end = end
139 | self.autoscale_radius = autoscale_radius
140 |
141 | # Convenience parameters
142 | self.phi = radians(self.rotation)
143 | self.rot_matrix = exp(1j * self.phi)
144 |
145 | # Derive derived parameters
146 | self._parameterize()
147 |
148 | def __repr__(self):
149 | params = (self.start, self.radius, self.rotation,
150 | self.large_arc, self.sweep, self.end)
151 | return ("Arc(start={}, radius={}, rotation={}, "
152 | "large_arc={}, sweep={}, end={})".format(*params))
153 |
154 | def __eq__(self, other):
155 | if not isinstance(other, Arc):
156 | return NotImplemented
157 | return self.start == other.start and self.end == other.end \
158 | and self.radius == other.radius \
159 | and self.rotation == other.rotation \
160 | and self.large_arc == other.large_arc and self.sweep == other.sweep
161 |
162 | def __ne__(self, other):
163 | if not isinstance(other, Arc):
164 | return NotImplemented
165 | return not self == other
166 |
167 | def _parameterize(self):
168 | # See http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
169 | # my notation roughly follows theirs
170 | rx = self.radius.real
171 | ry = self.radius.imag
172 | rx_sqd = rx * rx
173 | ry_sqd = ry * ry
174 |
175 | # Transform z-> z' = x' + 1j*y'
176 | # = self.rot_matrix**(-1)*(z - (end+start)/2)
177 | # coordinates. This translates the ellipse so that the midpoint
178 | # between self.end and self.start lies on the origin and rotates
179 | # the ellipse so that the its axes align with the xy-coordinate axes.
180 | # Note: This sends self.end to -self.start
181 | zp1 = (1 / self.rot_matrix) * (self.start - self.end) / 2
182 | x1p, y1p = zp1.real, zp1.imag
183 | x1p_sqd = x1p * x1p
184 | y1p_sqd = y1p * y1p
185 |
186 | # Correct out of range radii
187 | # Note: an ellipse going through start and end with radius and phi
188 | # exists if and only if radius_check is true
189 | radius_check = (x1p_sqd / rx_sqd) + (y1p_sqd / ry_sqd)
190 | if radius_check > 1:
191 | if self.autoscale_radius:
192 | rx *= sqrt(radius_check)
193 | ry *= sqrt(radius_check)
194 | self.radius = rx + 1j * ry
195 | rx_sqd = rx * rx
196 | ry_sqd = ry * ry
197 | else:
198 | raise ValueError("No such elliptic arc exists.")
199 |
200 | # Compute c'=(c_x', c_y'), the center of the ellipse in (x', y') coords
201 | # Noting that, in our new coord system, (x_2', y_2') = (-x_1', -x_2')
202 | # and our ellipse is cut out by of the plane by the algebraic equation
203 | # (x'-c_x')**2 / r_x**2 + (y'-c_y')**2 / r_y**2 = 1,
204 | # we can find c' by solving the system of two quadratics given by
205 | # plugging our transformed endpoints (x_1', y_1') and (x_2', y_2')
206 | tmp = rx_sqd * y1p_sqd + ry_sqd * x1p_sqd
207 | radicand = (rx_sqd * ry_sqd - tmp) / tmp
208 | try:
209 | radical = sqrt(radicand)
210 | except ValueError:
211 | radical = 0
212 | if self.large_arc == self.sweep:
213 | cp = -radical * (rx * y1p / ry - 1j * ry * x1p / rx)
214 | else:
215 | cp = radical * (rx * y1p / ry - 1j * ry * x1p / rx)
216 |
217 | # The center in (x,y) coordinates is easy to find knowing c'
218 | self.center = exp(1j * self.phi) * cp + (self.start + self.end) / 2
219 |
220 | # Now we do a second transformation, from (x', y') to (u_x, u_y)
221 | # coordinates, which is a translation moving the center of the
222 | # ellipse to the origin and a dilation stretching the ellipse to be
223 | # the unit circle
224 | u1 = (x1p - cp.real) / rx + 1j * (
225 | y1p - cp.imag) / ry # transformed start
226 | u2 = (-x1p - cp.real) / rx + 1j * (
227 | -y1p - cp.imag) / ry # transformed end
228 |
229 | # clip in case of floating point error
230 | u1 = clip(u1.real, -1, 1) + 1j * clip(u1.imag, -1, 1)
231 | u2 = clip(u2.real, -1, 1) + 1j * clip(u2.imag, -1, 1)
232 |
233 | # Now compute theta and delta (we'll define them as we go)
234 | # delta is the angular distance of the arc (w.r.t the circle)
235 | # theta is the angle between the positive x'-axis and the start point
236 | # on the circle
237 | if u1.imag > 0:
238 | self.theta = degrees(acos(u1.real))
239 | elif u1.imag < 0:
240 | self.theta = -degrees(acos(u1.real))
241 | else:
242 | if u1.real > 0: # start is on pos u_x axis
243 | self.theta = 0
244 | else: # start is on neg u_x axis
245 | # Note: This behavior disagrees with behavior documented in
246 | # http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
247 | # where theta is set to 0 in this case.
248 | self.theta = 180
249 |
250 | det_uv = u1.real * u2.imag - u1.imag * u2.real
251 |
252 | acosand = u1.real * u2.real + u1.imag * u2.imag
253 | acosand = clip(acosand.real, -1, 1) + clip(acosand.imag, -1, 1)
254 |
255 | if det_uv > 0:
256 | self.delta = degrees(acos(acosand))
257 | elif det_uv < 0:
258 | self.delta = -degrees(acos(acosand))
259 | else:
260 | if u1.real * u2.real + u1.imag * u2.imag > 0:
261 | # u1 == u2
262 | self.delta = 0
263 | else:
264 | # u1 == -u2
265 | # Note: This behavior disagrees with behavior documented in
266 | # http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
267 | # where delta is set to 0 in this case.
268 | self.delta = 180
269 |
270 | if not self.sweep and self.delta >= 0:
271 | self.delta -= 360
272 | elif self.large_arc and self.delta <= 0:
273 | self.delta += 360
274 |
275 | def point(self, t):
276 | if t == 0:
277 | return self.start
278 | if t == 1:
279 | return self.end
280 | angle = radians(self.theta + t * self.delta)
281 | cosphi = self.rot_matrix.real
282 | sinphi = self.rot_matrix.imag
283 | rx = self.radius.real
284 | ry = self.radius.imag
285 |
286 | # z = self.rot_matrix*(rx*cos(angle) + 1j*ry*sin(angle)) + self.center
287 | x = rx * cosphi * cos(angle) - ry * sinphi * sin(
288 | angle) + self.center.real
289 | y = rx * sinphi * cos(angle) + ry * cosphi * sin(
290 | angle) + self.center.imag
291 | return complex(x, y)
292 |
293 | def bbox(self):
294 | """returns a bounding box for the segment in the form
295 | (xmin, xmax, ymin, ymax)."""
296 | # a(t) = radians(self.theta + self.delta*t)
297 | # = (2*pi/360)*(self.theta + self.delta*t)
298 | # x'=0: ~~~~~~~~~
299 | # -rx*cos(phi)*sin(a(t)) = ry*sin(phi)*cos(a(t))
300 | # -(rx/ry)*cot(phi)*tan(a(t)) = 1
301 | # a(t) = arctan(-(ry/rx)tan(phi)) + pi*k === atan_x
302 | # y'=0: ~~~~~~~~~~
303 | # rx*sin(phi)*sin(a(t)) = ry*cos(phi)*cos(a(t))
304 | # (rx/ry)*tan(phi)*tan(a(t)) = 1
305 | # a(t) = arctan((ry/rx)*cot(phi))
306 | # atanres = arctan((ry/rx)*cot(phi)) === atan_y
307 | # ~~~~~~~~
308 | # (2*pi/360)*(self.theta + self.delta*t) = atanres + pi*k
309 | # Therefore, for both x' and y', we have...
310 | # t = ((atan_{x/y} + pi*k)*(360/(2*pi)) - self.theta)/self.delta
311 | # for all k s.t. 0 < t < 1
312 | from math import atan, tan
313 |
314 | if cos(self.phi) == 0:
315 | atan_x = pi / 2
316 | atan_y = 0
317 | elif sin(self.phi) == 0:
318 | atan_x = 0
319 | atan_y = pi / 2
320 | else:
321 | rx, ry = self.radius.real, self.radius.imag
322 | atan_x = atan(-(ry / rx) * tan(self.phi))
323 | atan_y = atan((ry / rx) / tan(self.phi))
324 |
325 | def angle_inv(ang, q): # inverse of angle from Arc.derivative()
326 | return ((ang + pi * q) * (360 / (2 * pi)) - self.theta) / self.delta
327 |
328 | xtrema = [self.start.real, self.end.real]
329 | ytrema = [self.start.imag, self.end.imag]
330 |
331 | for k in range(-4, 5):
332 | tx = angle_inv(atan_x, k)
333 | ty = angle_inv(atan_y, k)
334 | if 0 <= tx <= 1:
335 | xtrema.append(self.point(tx).real)
336 | if 0 <= ty <= 1:
337 | ytrema.append(self.point(ty).imag)
338 | return min(xtrema), max(xtrema), min(ytrema), max(ytrema)
339 |
340 |
341 | COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
342 | UPPERCASE = set('MZLHVCSQTA')
343 |
344 | COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
345 | FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
346 |
347 |
348 | def _tokenize_path(path_def):
349 | for x in COMMAND_RE.split(path_def):
350 | if x in COMMANDS:
351 | yield x
352 | for token in FLOAT_RE.findall(x):
353 | yield token
354 |
355 |
356 | def parse_path(pathdef, current_pos=0j):
357 | # In the SVG specs, initial movetos are absolute, even if
358 | # specified as 'm'. This is the default behavior here as well.
359 | # But if you pass in a current_pos variable, the initial moveto
360 | # will be relative to that current_pos. This is useful.
361 | elements = list(_tokenize_path(pathdef))
362 | # Reverse for easy use of .pop()
363 | elements.reverse()
364 | absolute = False
365 |
366 | segments = []
367 |
368 | start_pos = None
369 | command = None
370 |
371 | while elements:
372 |
373 | if elements[-1] in COMMANDS:
374 | # New command.
375 | command = elements.pop()
376 | absolute = command in UPPERCASE
377 | command = command.upper()
378 | else:
379 | # If this element starts with numbers, it is an implicit command
380 | # and we don't change the command. Check that it's allowed:
381 | if command is None:
382 | raise ValueError(
383 | "Unallowed implicit command in %s, position %s" % (
384 | pathdef, len(pathdef.split()) - len(elements)))
385 |
386 | if command == 'M':
387 | # Moveto command.
388 | x = elements.pop()
389 | y = elements.pop()
390 | pos = float(x) + float(y) * 1j
391 | if absolute:
392 | current_pos = pos
393 | else:
394 | current_pos += pos
395 |
396 | # when M is called, reset start_pos
397 | # This behavior of Z is defined in svg spec:
398 | # http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
399 | start_pos = current_pos
400 |
401 | # Implicit moveto commands are treated as lineto commands.
402 | # So we set command to lineto here, in case there are
403 | # further implicit commands after this moveto.
404 | command = 'L'
405 |
406 | elif command == 'Z':
407 | # Close path
408 | if not (current_pos == start_pos):
409 | segments.append(Line(current_pos, start_pos))
410 | current_pos = start_pos
411 | command = None
412 |
413 | elif command == 'L':
414 | x = elements.pop()
415 | y = elements.pop()
416 | pos = float(x) + float(y) * 1j
417 | if not absolute:
418 | pos += current_pos
419 | segments.append(Line(current_pos, pos))
420 | current_pos = pos
421 |
422 | elif command == 'H':
423 | x = elements.pop()
424 | pos = float(x) + current_pos.imag * 1j
425 | if not absolute:
426 | pos += current_pos.real
427 | segments.append(Line(current_pos, pos))
428 | current_pos = pos
429 |
430 | elif command == 'V':
431 | y = elements.pop()
432 | pos = current_pos.real + float(y) * 1j
433 | if not absolute:
434 | pos += current_pos.imag * 1j
435 | segments.append(Line(current_pos, pos))
436 | current_pos = pos
437 |
438 | elif command == 'C':
439 | for i in range(4):
440 | # ignore control points
441 | elements.pop()
442 | end = float(elements.pop()) + float(elements.pop()) * 1j
443 |
444 | if not absolute:
445 | end += current_pos
446 |
447 | segments.append(Line(current_pos, end))
448 | current_pos = end
449 |
450 | elif command == 'S':
451 | for i in range(2):
452 | # ignore control points
453 | elements.pop()
454 | end = float(elements.pop()) + float(elements.pop()) * 1j
455 |
456 | if not absolute:
457 | end += current_pos
458 |
459 | segments.append(Line(current_pos, end))
460 | current_pos = end
461 |
462 | elif command == 'Q':
463 | for i in range(2):
464 | # ignore control points
465 | elements.pop()
466 | end = float(elements.pop()) + float(elements.pop()) * 1j
467 |
468 | if not absolute:
469 | end += current_pos
470 |
471 | segments.append(Line(current_pos, end))
472 | current_pos = end
473 |
474 | elif command == 'T':
475 |
476 | end = float(elements.pop()) + float(elements.pop()) * 1j
477 |
478 | if not absolute:
479 | end += current_pos
480 |
481 | segments.append(Line(current_pos, end))
482 | current_pos = end
483 |
484 | elif command == 'A':
485 | radius = float(elements.pop()) + float(elements.pop()) * 1j
486 | rotation = float(elements.pop())
487 | arc = float(elements.pop())
488 | sweep = float(elements.pop())
489 | end = float(elements.pop()) + float(elements.pop()) * 1j
490 |
491 | if not absolute:
492 | end += current_pos
493 |
494 | segments.append(Arc(current_pos, radius, rotation, arc, sweep, end))
495 | current_pos = end
496 |
497 | return segments
498 |
--------------------------------------------------------------------------------
/Stretch/kiplug/text.py:
--------------------------------------------------------------------------------
1 | from .colour import Colour
2 |
3 | #https://github.com/KiCad/kicad-source-mirror/blob/93466fa1653191104c5e13231dfdc1640b272777/pcbnew/plugins/kicad/pcb_parser.cpp#L2353
4 |
5 | # 0 gr_text
6 | # 1 text
7 | # 2
8 | # 0 at
9 | # 1 66.66
10 | # 2 99.99
11 | # 3
12 | # 0 layer
13 | # 1 F.SilkS
14 | # 4
15 | # 0 hide
16 | # 5
17 | # 0 tstamp
18 | # 1 5E451B20
19 | # 6
20 | # 0 effects
21 | # 1
22 | # 0 font
23 | # 1
24 | # 0 size
25 | # 1 1.5
26 | # 2 1.5
27 | # 2
28 | # 0 thickness
29 | # 1 0.3
30 | # 3
31 | # 0 bold
32 | # 4
33 | # 0 italic
34 | # 2
35 | # 0 justify
36 | # 1 mirror
37 | #
38 | # ---
39 | # 0 fp_text
40 | # 1 reference / value / user
41 | # 2 text
42 | # 3
43 | # 0 at
44 |
45 | pxToMM = 96 / 25.4
46 |
47 | def is_float_try(str):
48 | try:
49 | float(str)
50 | return True
51 | except ValueError:
52 | return False
53 |
54 | class Text(object):
55 |
56 | def __init__(self):
57 | self.type = 'gr_text'
58 | self.reference = ''
59 | self.text = ''
60 | self.at = []
61 | self.layer = ''
62 | self.hide = False
63 | self.tstamp = ''
64 |
65 | #Effects params
66 | self.size = [1.27, 1.27]
67 | self.thickness = 0
68 | self.bold = False
69 | self.italic = False
70 | # Left, right, top, bottom, or mirror
71 | self.justify = ''
72 |
73 |
74 |
75 | def From_PCB(self, input):
76 |
77 | #gr_text is user-created label, fp_text is module ref/value
78 | if input[0] == 'gr_text':
79 | self.reference = 'gr_text'
80 | self.text = input[1]
81 | self.type = 'gr_text'
82 |
83 | elif input[0] == 'fp_text':
84 | self.reference = input[1]
85 | self.text = input[2]
86 | self.type = 'fp_text'
87 |
88 | for item in input:
89 | if type(item) == str:
90 | if item == 'hide':
91 | self.hide = True
92 | else:
93 | if item[0] == 'at':
94 | self.at = item[1:]
95 |
96 | if item[0] == 'layer':
97 | self.layer = item[1]
98 |
99 | if item[0] == 'tstamp':
100 | self.tstamp = item[1]
101 |
102 | if item[0] == 'effects':
103 | for effect in item[1:]:
104 | if effect[0] == 'font':
105 | for param in effect[1:]:
106 | if param[0] == 'size':
107 | self.size = [param[1], param[2]]
108 | if param[0] == 'thickness':
109 | self.thickness = param[1]
110 | elif effect[0] == 'justify':
111 | self.justify = effect[1]
112 |
113 |
114 | def To_PCB(self):
115 | pcb = [self.type]
116 |
117 | if self.reference != "":
118 | pcb.append(self.reference)
119 |
120 | pcb.append(self.text)
121 |
122 | at = ['at'] + self.at
123 |
124 | pcb.append(at)
125 |
126 | pcb.append(self.layer)
127 |
128 | if self.hide == True:
129 | pcb.append("hide")
130 |
131 | if self.tstamp:
132 | pcb.append(['tstamp', self.tstamp])
133 |
134 | font = ['font', ['size'] + self.size, ['thickness', self.thickness]]
135 |
136 | effects = ['effects', font]
137 |
138 | if self.justify:
139 | justify = ['justify', self.justify]
140 | effects.append(justify)
141 |
142 | pcb.append(effects)
143 |
144 |
145 |
146 | return pcb
147 |
148 |
149 | def From_SVG(self, tag, angle = 0):
150 | angle = int(angle)
151 | text = []
152 |
153 | if tag.has_attr('type'):
154 | if tag['type'] == 'gr_text':
155 | self.type = 'gr_text'
156 | else:
157 | self.type = 'fp_text'
158 | self.reference = tag['reference']
159 |
160 | self.text = tag.contents[0]
161 | # print(tag.contents)
162 |
163 | x = 0
164 | y = 0
165 |
166 | if tag.has_attr('x'):
167 | x = float(tag['x']) / pxToMM
168 | if tag.has_attr('y'):
169 | y = float(tag['y']) / pxToMM
170 |
171 | if tag.has_attr('mirrored'):
172 | if tag['mirrored'] == 'true':
173 | self.justify = ['mirror']
174 | x = float(x) * -1.0
175 |
176 |
177 |
178 | if tag.has_attr('layer'):
179 | layer = ['layer', tag['layer']]
180 | elif tag.parent.has_attr('inkscape:label'):
181 | #XML metadata trashed, try to recover from parent tag
182 | layer = ['layer', tag.parent['inkscape:label']]
183 | else:
184 | assert False, "Text not in layer"
185 |
186 | self.layer = layer
187 |
188 | rotate = 0
189 | if tag.has_attr('transform'):
190 | transform = tag['transform']
191 | if 'translate(' in transform:
192 | translate = transform[transform.find('translate(') + 10:]
193 | translate = translate[:translate.find(')')]
194 | if ',' in translate:
195 | x_t, y_t = translate.split(',')
196 | else:
197 | x_t = translate
198 | y_t = 0
199 | x += float(x_t) / pxToMM
200 | y += float(y_t) / pxToMM
201 | if 'rotate(' in transform:
202 | rotate = transform[transform.find('rotate(') + 7:]
203 | rotate = rotate[:rotate.find(')')]
204 | if ',' in rotate:
205 | rotate = rotate[:rotate.find(',')]
206 | angle += float(rotate)
207 |
208 | self.at = [str(x), str(y)]
209 |
210 | if angle != 0:
211 | self.at.append(str(angle))
212 |
213 | if tag.has_attr('hide'):
214 | if tag['hide'] == 'True':
215 | self.hide = True
216 |
217 | if tag.has_attr('tstamp'):
218 | self.tstamp = tag['tstamp']
219 |
220 | style = tag['style']
221 |
222 | styletag = style[style.find('font-size:') + 10:]
223 |
224 | size = styletag[0:styletag.find('px')]
225 | size = str(float(size) / pxToMM)
226 |
227 | self.size = [size, size]
228 | self.thickness = tag['thickness']
229 | self.justify = tag['justify']
230 | if tag.has_attr('bold'):
231 | self.bold = True
232 | if tag.has_attr('italic'):
233 | self.italic = True
234 |
235 |
236 | def To_SVG(self, angle = 0, hiddenLayers = []):
237 | # transform = 'scale(-1) '
238 | transform = ''
239 | angle = float(angle)
240 |
241 | transform += "translate(" + str(float(self.at[0]) * pxToMM) + "," + str(float(self.at[1]) * pxToMM) + ")"
242 | if len(self.at) > 2:
243 | if is_float_try(self.at[2]):
244 | angle += float(self.at[2])
245 | if angle != 0:
246 | transform += ' rotate(' + str(angle)+ ')'
247 | # self.tstamp = 'tstamp="' + item[1] + '" '
248 | # if item[0] == 'effects':
249 | # for effect in item[1:]:
250 | # if effect[0] == 'font':
251 | # for param in effect[1:]:
252 | # if param[0] == 'size':
253 | # size = [param[1], param[2]]
254 | # if param[0] == 'thickness':
255 | # thickness = param[1]
256 | # elif effect[0] == 'justify':
257 | # if len(effect) > 1:
258 | # if effect[1] == 'mirror':
259 | # transform += ' scale(-1,1)'
260 | # mirror = -1
261 | # mirror_text = 'mirrored="true" '
262 |
263 | # else:
264 | # effect_text = 'effects="' + ';'.join(effect) + '" '
265 |
266 | mirror_text = ''
267 | if self.justify == 'mirror':
268 | transform += ' scale(-1,1)'
269 | mirror_text = 'mirrored="true" '
270 |
271 | if len(transform) > 0:
272 | transform = 'transform="' + transform + '" '
273 |
274 | hidelayer = ''
275 | if self.layer in hiddenLayers:
276 | hidelayer = ';display:none'
277 |
278 | hide = ''
279 | if self.hide == True:
280 | hide = 'hide="True" '
281 | hidelayer = ';display:none'
282 |
283 | parameters = '' + self.text
306 | parameters += ''
307 |
308 | return parameters
309 |
--------------------------------------------------------------------------------
/Stretch/kiplug/via.py:
--------------------------------------------------------------------------------
1 |
2 | from .colour import Colour
3 |
4 | #https://github.com/KiCad/kicad-source-mirror/blob/93466fa1653191104c5e13231dfdc1640b272777/pcbnew/plugins/kicad/pcb_parser.cpp#L4275
5 |
6 | # 0 via
7 | # 1
8 | # 0 at
9 | # 1 66.66
10 | # 2 99.99
11 | # 2
12 | # 0 size
13 | # 1 0.6
14 | # 3
15 | # 0 drill
16 | # 1 0.3
17 | # 4
18 | # 0 layers
19 | # 1 F.Cu
20 | # 2 B.Cu
21 | # 5
22 | # 0 net
23 | # 1 16
24 |
25 |
26 | pxToMM = 96 / 25.4
27 |
28 | class Via(object):
29 |
30 | def __init__(self):
31 | self.blind = False
32 | self.micro = False
33 | self.at = []
34 | self.size = 0
35 | self.drill = 0
36 | self.layers = []
37 | self.net = 0
38 | self.remove_unused_layers = False
39 | self.keep_end_layers = False
40 | self.tstamp = ''
41 | self.status = ''
42 | self.locked = False
43 | self.free = False
44 |
45 |
46 | def From_SVG(self, tag):
47 | x = float(tag['x'])
48 | y = float(tag['y'])
49 | if tag.has_attr('transform'):
50 | transform = tag['transform']
51 | translate = transform[transform.find('translate(') + 10:-1]
52 | translate = translate[0:translate.find(')')]
53 | xt, yt = translate.split(',')
54 | x = (float(xt) + x)
55 | y = (float(yt) + y)
56 | self.at = [str(x / pxToMM), str(y / pxToMM)]
57 |
58 | self.size = str(float(tag['size']) / pxToMM)
59 | self.drill = str(float(tag['drill']) / pxToMM)
60 |
61 | self.layers = tag['layers'].split(',')
62 |
63 | self.net = tag['net']
64 |
65 | if tag.has_attr('tstamp'):
66 | self.tstamp = tag['tstamp']
67 | if tag.has_attr('status'):
68 | self.status = tag['status']
69 | if tag.has_attr('blind'):
70 | self.blind = True
71 | if tag.has_attr('micro'):
72 | self.micro = True
73 | if tag.has_attr('remove_unused_layers'):
74 | self.remove_unused_layers = True
75 | if tag.has_attr('keep_end_layers'):
76 | self.keep_end_layers = True
77 | if tag.has_attr('locked'):
78 | self.locked = True
79 | if tag.has_attr('free'):
80 | self.free = True
81 |
82 | def To_PCB(self):
83 | at = ['at'] + self.at
84 |
85 | via = [ 'via', at, ['size', self.size], ['drill', self.drill]]
86 |
87 | via.append(['layers'] + self.layers)
88 |
89 | via.append(['net', self.net])
90 |
91 | if self.tstamp != '':
92 | via.append(['tstamp', self.tstamp])
93 | if self.status != '':
94 | via.append(['status', self.status])
95 | if self.blind != False:
96 | via.insert(1, 'blind')
97 | if self.micro != False:
98 | via.insert(1, 'micro')
99 | if self.remove_unused_layers != False:
100 | via.append(['remove_unused_layers'])
101 | if self.keep_end_layers != False:
102 | via.append(['keep_end_layers'])
103 | if self.locked != False:
104 | via.append(['locked'])
105 | if self.free != False:
106 | via.append(['free'])
107 |
108 | return via
109 |
110 |
111 | def From_PCB(self, pcblist):
112 | at = []
113 | layers = []
114 | blind = ''
115 | status = ''
116 | tstamp = ''
117 |
118 | if pcblist[0] != 'via':
119 | assert False,"Via: Not a via"
120 | return None
121 |
122 | for item in pcblist:
123 | if item[0] == 'at':
124 | self.at = [item[1], item[2]]
125 |
126 | if item[0] == 'size':
127 | self.size = item[1]
128 |
129 | if item[0] == 'drill':
130 | self.drill = item[1]
131 |
132 | if item[0] == 'layers':
133 | self.layers = [item[1], item[2]]
134 |
135 | if item[0] == 'net':
136 | self.net = item[1]
137 |
138 | if item == 'blind':
139 | self.blind = True
140 |
141 | if item[0] == 'tstamp':
142 | self.tstamp = item[1]
143 |
144 | if item[0] == 'status':
145 | self.status = item[1]
146 |
147 |
148 | def To_SVG(self):
149 | tstamp = ''
150 | status = ''
151 | blind = ''
152 | micro = ''
153 | remove_unused_layers = ''
154 | keep_end_layers = ''
155 | locked = ''
156 | free = ''
157 |
158 | if self.tstamp != '':
159 | tstamp = 'tstamp="' + self.tstamp + '" '
160 | if self.status != '':
161 | status = 'status="' + self.status + '" '
162 | if self.blind != False:
163 | blind = 'blind="true" '
164 | if self.micro != False:
165 | micro = 'micro="true" '
166 | if self.remove_unused_layers != False:
167 | remove_unused_layers = 'remove_unused_layers="true" '
168 | if self.keep_end_layers != False:
169 | keep_end_layers = 'keep_end_layers="true" '
170 | if self.locked != False:
171 | locked = 'locked="true" '
172 | if self.free != False:
173 | free = 'free="true" '
174 |
175 | parameters = ''
193 |
194 | hole = ''
203 |
204 | parameters += '' + hole
224 |
225 | parameters += ''
226 |
227 | #print(parameters)
228 | return parameters
229 |
--------------------------------------------------------------------------------
/Stretch/kiplug/zone.py:
--------------------------------------------------------------------------------
1 | # https://github.com/KiCad/kicad-source-mirror/blob/93466fa1653191104c5e13231dfdc1640b272777/pcbnew/plugins/kicad/pcb_parser.cpp#L4386
2 |
3 | from .colour import Colour
4 | from .metadata import Metadata
5 | from .svgpath import parse_path
6 |
7 |
8 | # 0 zone
9 | # 1
10 | # 0 net
11 | # 1 16
12 | # 2
13 | # 0 net_name
14 | # 1 GND
15 | # 3
16 | # 0 layer
17 | # 1 B.Cu
18 | # 4
19 | # 0 tstamp
20 | # 1 5EACCA92
21 | # 5
22 | # 0 hatch
23 | # 1 edge
24 | # 2 0.508
25 | # 6
26 | # 0 connect_pads
27 | # 1
28 | # 0 clearance
29 | # 1 0.1524
30 | # 7
31 | # 0 min_thickness
32 | # 1 0.1524
33 | # 8
34 | # 0 fill
35 | # 1 yes
36 | # 2
37 | # 0 arc_segments
38 | # 1 32
39 | # 3
40 | # 0 thermal_gap
41 | # 1 0.1524
42 | # 4
43 | # 0 thermal_bridge_width
44 | # 1 0.1525
45 | # 9
46 | # 0 polygon
47 | # 1
48 | # 0 pts
49 | # 1
50 | # 0 xy
51 | # 1 147.6375
52 | # 2 120.9675
53 | # 2
54 | # 0 xy
55 | # 1 147.6375
56 | # 2 120.9675
57 | # 3
58 | # ...
59 | # 10
60 | # 0 filled_polygon
61 | # 1
62 | # 0 pts
63 | # 1
64 | # 0 xy
65 | # 1 147.6375
66 | # 2 120.9675
67 | # 2
68 | # 0 xy
69 | # 1 147.6375
70 | # 2 120.9675
71 | # 3
72 | # ...
73 |
74 | pxToMM = 96 / 25.4
75 |
76 |
77 | class Zone(object):
78 |
79 | def __init__(self):
80 | self.net = ''
81 | self.net_name = ''
82 | self.layers = ''
83 | self.tstamp = ''
84 | self.hatch = ''
85 | self.priority = 0
86 | self.connect_pads = ''
87 | self.min_thickness = ''
88 | self.filled_areas_thickness = ''
89 | self.fill = ''
90 | self.keepout = ''
91 | self.polygon = []
92 | self.filled_polygon = []
93 | self.filled_segments = []
94 | self.name = ''
95 | self.island = False
96 |
97 | # Most of these don't need to be handled
98 | self.metadata = []
99 |
100 | def From_PCB(self, input):
101 | # print(input)
102 |
103 | for item in input:
104 |
105 | if item[0] == 'net':
106 | self.net = item[1]
107 |
108 | elif item[0] == 'net_name':
109 | self.net_name = item[1]
110 |
111 | elif item[0] == 'layer':
112 | self.layers = item[1]
113 |
114 | elif item[0] == 'layers':
115 | for layer in item[1:]:
116 | self.layers += layer + ' '
117 |
118 | elif item[0] == 'tstamp':
119 | self.tstamp = item[1]
120 |
121 | elif item[0] == 'hatch':
122 | self.hatch = item[1:]
123 |
124 | elif item[0] == 'priority':
125 | self.priority = item[1]
126 |
127 | elif item[0] == 'connect_pads':
128 | self.connect_pads = item[1]
129 |
130 | elif item[0] == 'min_thickness':
131 | self.min_thickness = item[1]
132 |
133 | elif item[0] == 'filled_areas_thickness':
134 | self.filled_areas_thickness = item[1]
135 |
136 | elif item[0] == 'fill':
137 | for x in item[1:]:
138 | if type(x) == str:
139 | self.fill = x
140 |
141 | elif item[0] == 'keepout':
142 | self.keepout = item[1]
143 |
144 | elif item[0] == 'island':
145 | self.island = True
146 |
147 | elif item[0] == 'name':
148 | self.name = item[1]
149 |
150 | elif item[0] == 'polygon':
151 | for xy in item[1]:
152 | if xy[0] == 'xy':
153 | self.polygon.append([xy[1], xy[2]])
154 |
155 | elif item[0] == 'filled_polygon':
156 | for xy in item[1]:
157 | if xy[0] == 'xy':
158 | self.filled_polygon.append([xy[1], xy[2]])
159 |
160 | elif item[0] == 'filled_segments':
161 | for xy in item[1]:
162 | if xy[0] == 'xy':
163 | self.filled_segments.append([xy[1], xy[2]])
164 |
165 |
166 |
167 | def To_PCB(self):
168 | pcb = ['zone']
169 |
170 | if self.net:
171 | pcb.append(['net', self.net])
172 | if self.net_name:
173 | pcb.append(['net_name', self.net_name])
174 | if self.layers:
175 | pcb.append(['layers'] + self.layers.split())
176 | if self.island:
177 | pcb.append(['island'])
178 | if self.tstamp:
179 | pcb.append(['tstamp', self.tstamp])
180 | if self.hatch:
181 | pcb.append(['hatch'] + self.hatch)
182 | if self.priority:
183 | pcb.append(['priority', self.priority])
184 | if self.connect_pads:
185 | pcb.append(['connect_pads', self.connect_pads])
186 | if self.min_thickness:
187 | pcb.append(['min_thickness', self.min_thickness])
188 | if self.filled_areas_thickness:
189 | pcb.append(['filled_areas_thickness', self.filled_areas_thickness])
190 | if self.fill:
191 | pcb.append(['fill', self.fill])
192 | if self.keepout:
193 | pcb.append(['keepout', self.keepout])
194 |
195 | if len(self.polygon) > 0:
196 | polygon = ['pts']
197 | for item in self.polygon:
198 | pt = ['xy'] + item
199 | polygon.append(pt)
200 | polygon = ['polygon'] + [polygon]
201 | pcb.append(polygon)
202 |
203 | if len(self.filled_polygon) > 0:
204 | filled_polygon = ['filled_polygon']
205 | for item in self.filled_polygon:
206 | xy = ['xy'] + item
207 | filled_polygon += [xy]
208 | pcb.append(filled_polygon)
209 |
210 | if len(self.filled_segments) > 0:
211 | filled_segments = ['filled_segments']
212 | for item in self.filled_segments:
213 | xy = ['xy'] + item
214 | filled_segments += [xy]
215 | pcb.append(filled_segments)
216 |
217 | if self.name:
218 | pcb.append(['name', self.name])
219 |
220 | return pcb
221 |
222 | def To_SVG(self):
223 |
224 | xy_text = ''
225 |
226 | if len(self.polygon) > 0:
227 | for xy in self.polygon:
228 | xy_text += ' ' + str(float(xy[0]) * pxToMM)
229 | xy_text += ',' + str(float(xy[1]) * pxToMM)
230 |
231 |
232 |
233 | # We don't really care about accurate fill zones in SVG
234 | # elif len(self.filled_polygon) > 0:
235 | # for filled_polygon in self.filled_polygon:
236 | # xy_text = ''
237 | # for xy in filled_polygon:
238 | # xy_text += ' ' + str(float(xy[0]) * pxToMM)
239 | # xy_text += ',' + str(float(xy[1]) * pxToMM)
240 |
241 |
242 |
243 | net = ''
244 | if self.net != '':
245 | net = 'net="' + self.net + '" '
246 |
247 | net_name = ''
248 | if self.net_name != '':
249 | net_name = 'net_name="' + self.net_name + '" '
250 |
251 | tstamp = ''
252 | if self.tstamp != '':
253 | tstamp = 'tstamp="' + self.tstamp + '" '
254 |
255 | hatch = ''
256 | if self.hatch != '':
257 | hatch = 'hatch="' + self.hatch[0] + '" '
258 | hatch += 'hatch_distance="' + self.hatch[1] + '" '
259 |
260 | priority = ''
261 | if self.priority != '':
262 | priority = 'priority="' + str(self.priority) + '" '
263 |
264 | connect_pads = ''
265 | # if self.connect_pads != '':
266 | # connect_pads = 'connect_pads="' + self.connect_pads + '" '
267 |
268 | min_thickness = ''
269 | if self.min_thickness != '':
270 | min_thickness = 'min_thickness="' + self.min_thickness + '" '
271 |
272 | filled_areas_thickness = ''
273 | if self.filled_areas_thickness != '':
274 | filled_areas_thickness = 'filled_areas_thickness="' + self.filled_areas_thickness + '" '
275 |
276 |
277 | layers = ''
278 | if self.layers != '':
279 | layers = 'layers="' + self.layers + '" '
280 |
281 | fill = ''
282 | if self.fill != '':
283 | fill = 'fill="' + self.fill + '" '
284 |
285 | keepout = ''
286 | if self.keepout != '':
287 | keepout = 'keepout="' + self.keepout + '" '
288 |
289 | island = ''
290 | if self.island == True:
291 | island = 'island="True" '
292 |
293 | name = ''
294 | if self.name != '':
295 | name = 'name="' + self.name + '" '
296 |
297 | parameters = ''
316 | parameters += ''
317 |
318 | # print(parameters)
319 | return parameters
320 |
321 |
322 |
323 | def From_SVG(self, tag, path):
324 | data = [tag['type']]
325 | style = tag['style']
326 |
327 | styletag = style[style.find('stroke-width:') + 13:]
328 | width = styletag[0:styletag.find('mm')]
329 |
330 | path = parse_path(tag['d'])
331 |
332 | pts = []
333 | for point in path:
334 | xy = []
335 | xy.append(str(point.start.real / pxToMM))
336 | xy.append(str(point.start.imag / pxToMM))
337 | pts.append(xy)
338 |
339 | xy = []
340 | xy.append(str(path[0].start.real / pxToMM))
341 | xy.append(str(path[0].start.imag / pxToMM))
342 | pts.append(xy)
343 |
344 | if tag.has_attr('layers'):
345 | self.layers = tag['layers']
346 | if tag.has_attr('net'):
347 | self.net = tag['net']
348 | if tag.has_attr('net_name'):
349 | self.net_name = tag['net_name']
350 | if tag.has_attr('tstamp'):
351 | self.tstamp = tag['tstamp']
352 | if tag.has_attr('hatch'):
353 | self.hatch = [tag['hatch'], tag['hatch_distance']]
354 | if tag.has_attr('priority'):
355 | self.priority = tag['priority']
356 | if tag.has_attr('connect_pads'):
357 | self.connect_pads = tag['connect_pads']
358 | if tag.has_attr('min_thickness'):
359 | self.min_thickness = tag['min_thickness']
360 | if tag.has_attr('filled_areas_thickness'):
361 | self.filled_areas_thickness = tag['filled_areas_thickness']
362 | if tag.has_attr('fill'):
363 | self.fill = tag['fill']
364 | if tag.has_attr('island'):
365 | self.fill = True
366 | if tag.has_attr('keepout'):
367 | self.keepout = tag['keepout']
368 | if tag.has_attr('name'):
369 | self.name = tag['name']
370 |
371 | self.polygon = pts
372 | self.width = width
373 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | from .Stretch import plugin_svg
2 | from .Stretch import plugin_pcb
--------------------------------------------------------------------------------
/docs/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/docs/1.png
--------------------------------------------------------------------------------
/docs/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/docs/2.png
--------------------------------------------------------------------------------
/docs/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/docs/3.png
--------------------------------------------------------------------------------
/docs/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/docs/4.png
--------------------------------------------------------------------------------
/docs/inkscape-close-in.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/docs/inkscape-close-in.png
--------------------------------------------------------------------------------
/docs/inkscape-close-out.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/docs/inkscape-close-out.png
--------------------------------------------------------------------------------
/docs/inkscape1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/docs/inkscape1.png
--------------------------------------------------------------------------------
/docs/kicad-close-in.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/docs/kicad-close-in.png
--------------------------------------------------------------------------------
/docs/kicad-close-out.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/docs/kicad-close-out.png
--------------------------------------------------------------------------------
/docs/kicad1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/docs/kicad1.png
--------------------------------------------------------------------------------
/docs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/docs/logo.png
--------------------------------------------------------------------------------
/pcm/build.py:
--------------------------------------------------------------------------------
1 | # Thx Greg Davill
2 | # https://github.com/gregdavill/KiBuzzard
3 |
4 |
5 | import os
6 | from os import path
7 | import shutil
8 |
9 | src_path = path.join(os.path.abspath(path.dirname(__file__)),'..','Stretch')
10 |
11 | metadata_template = path.join(os.path.abspath(path.dirname(__file__)),'metadata_template.json')
12 | resources_path = path.join(os.path.abspath(path.dirname(__file__)),'resources')
13 | print(src_path)
14 |
15 | build_path = os.path.abspath(path.join('build'))
16 |
17 | try:
18 | shutil.rmtree(build_path)
19 | except FileNotFoundError:
20 | pass
21 | os.mkdir(build_path)
22 | os.mkdir(path.join(build_path,'plugin'))
23 | os.chdir(build_path)
24 |
25 | shutil.copytree(src_path, path.join(os.path.abspath('plugin'),'plugins'))
26 |
27 | # clean out any __pycache__ or .pyc files (https://stackoverflow.com/a/41386937)
28 | import pathlib
29 | [p.unlink() for p in pathlib.Path('.').rglob('*.py[co]')]
30 | [p.rmdir() for p in pathlib.Path('.').rglob('__pycache__')]
31 |
32 |
33 | # copy metadata
34 | shutil.copy(metadata_template, path.join('plugin','metadata.json'))
35 | # copy icon
36 | shutil.copytree(resources_path, path.join('plugin','resources'))
37 |
38 | # load up json script
39 | from pathlib import Path
40 | import json
41 | with open(metadata_template) as f:
42 | md = json.load(f)
43 |
44 |
45 |
46 | # zip all files
47 | zip_file = 'Stretch-{0}-pcm.zip'.format(md['versions'][0]['version'])
48 | shutil.make_archive(Path(zip_file).stem, 'zip', 'plugin')
49 |
50 |
51 | zip_size = path.getsize(zip_file)
52 |
53 |
54 | uncompressed_size = sum(f.stat().st_size for f in Path('plugin').glob('**/*') if f.is_file())
55 |
56 | import hashlib
57 | with open(zip_file, 'rb') as f:
58 | zip_sha256 = hashlib.sha256(f.read()).hexdigest()
59 |
60 |
61 |
62 | md['versions'][0].update({
63 | 'install_size': uncompressed_size,
64 | 'download_size': zip_size,
65 | 'download_sha256': zip_sha256,
66 | 'download_url': 'https://github.com/JarrettR/Stretch/releases/download/{0}/Stretch-{0}-pcm.zip'.format(md['versions'][0]['version'])
67 | })
68 |
69 | with open('metadata.json', 'w') as of:
70 | json.dump(md, of, indent=2)
71 |
--------------------------------------------------------------------------------
/pcm/metadata_template.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://go.kicad.org/pcm/schemas/v1",
3 | "name": "Stretch",
4 | "description": "KiCad to SVG and then back again",
5 | "description_full": "Seamless artistic workflow for KiCad and Inkscape.\n\nUsers can start by drawing a schematic and laying out a PCB, then bring it into Inkscape to arrange a thousand LEDs into a flower arrangement, then bring it back into KiCad to lay out traces, back into Inkscape to curve the traces, back into KiCad to change their microcontroller and few pin assignments, back into Inkscape to draw out some silkscreem patterns, back into KiCad to run DRC, and so on. The workflow is intended to be seamless and painless to go back and forth.\n\nClick the homepage link - https://github.com/JarrettR/Stretch - to see how it works.",
6 | "identifier": "com.github.jarrettr.stretch",
7 | "type": "plugin",
8 | "author": { "name": "Jarrett Rainier",
9 |
10 | "contact": {
11 | "github": "https://github.com/JarrettR",
12 | "web": "https://jrainimo.com"
13 | }
14 | },
15 | "maintainer": {
16 | "name": "Jarrett Rainier",
17 | "contact": {
18 | "github": "https://github.com/JarrettR",
19 | "web": "https://jrainimo.com"
20 | }
21 | },
22 | "license": "GPL-3.0",
23 | "resources": {
24 | "homepage": "https://github.com/JarrettR/Stretch"
25 | },
26 | "versions": [
27 | {
28 | "version": "1.2",
29 | "status": "stable",
30 | "kicad_version": "6.0"
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/pcm/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JarrettR/Stretch/99f8441141ae9d3cc296a75b5cf3767067499bb1/pcm/resources/icon.png
--------------------------------------------------------------------------------
/tests/base.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
52 |
--------------------------------------------------------------------------------
/tests/simple.kicad_pcb:
--------------------------------------------------------------------------------
1 | (kicad_pcb (version 20171130) (host pcbnew 5.1.10-88a1d61d58~89~ubuntu20.04.1)
2 |
3 | (general
4 | (thickness 1.6)
5 | (drawings 19)
6 | (tracks 28)
7 | (zones 0)
8 | (modules 5)
9 | (nets 7)
10 | )
11 |
12 | (page User 132.004 102.006)
13 | (title_block
14 | (title "SOT23 to DIP Breakout Board")
15 | (date 2020-02-13)
16 | (rev 2)
17 | (company SirBoard)
18 | (comment 1 "SOT23-6 P = 0.95mm")
19 | (comment 2 "SC-70 P = 0.65mm")
20 | (comment 3 "DIP6 P = 2.54mm")
21 | )
22 |
23 | (layers
24 | (0 F.Cu signal)
25 | (31 B.Cu signal)
26 | (32 B.Adhes user hide)
27 | (33 F.Adhes user hide)
28 | (34 B.Paste user hide)
29 | (35 F.Paste user hide)
30 | (36 B.SilkS user)
31 | (37 F.SilkS user)
32 | (38 B.Mask user hide)
33 | (39 F.Mask user hide)
34 | (40 Dwgs.User user)
35 | (41 Cmts.User user hide)
36 | (42 Eco1.User user hide)
37 | (43 Eco2.User user hide)
38 | (44 Edge.Cuts user)
39 | (45 Margin user hide)
40 | (46 B.CrtYd user hide)
41 | (47 F.CrtYd user hide)
42 | (48 B.Fab user hide)
43 | (49 F.Fab user hide)
44 | )
45 |
46 | (setup
47 | (last_trace_width 0.25)
48 | (user_trace_width 0.2)
49 | (trace_clearance 0.2)
50 | (zone_clearance 0.508)
51 | (zone_45_only no)
52 | (trace_min 0.2)
53 | (via_size 0.8)
54 | (via_drill 0.4)
55 | (via_min_size 0.4)
56 | (via_min_drill 0.3)
57 | (uvia_size 0.3)
58 | (uvia_drill 0.1)
59 | (uvias_allowed no)
60 | (uvia_min_size 0.2)
61 | (uvia_min_drill 0.1)
62 | (edge_width 0.05)
63 | (segment_width 0.2)
64 | (pcb_text_width 0.3)
65 | (pcb_text_size 1.5 1.5)
66 | (mod_edge_width 0.12)
67 | (mod_text_size 1 1)
68 | (mod_text_width 0.15)
69 | (pad_size 1.7 1.7)
70 | (pad_drill 1)
71 | (pad_to_mask_clearance 0)
72 | (solder_mask_min_width 0.1)
73 | (aux_axis_origin 0 0)
74 | (visible_elements 7FFFFFFF)
75 | (pcbplotparams
76 | (layerselection 0x010f0_ffffffff)
77 | (usegerberextensions false)
78 | (usegerberattributes false)
79 | (usegerberadvancedattributes false)
80 | (creategerberjobfile false)
81 | (excludeedgelayer true)
82 | (linewidth 0.100000)
83 | (plotframeref false)
84 | (viasonmask false)
85 | (mode 1)
86 | (useauxorigin true)
87 | (hpglpennumber 1)
88 | (hpglpenspeed 20)
89 | (hpglpendiameter 15.000000)
90 | (psnegative false)
91 | (psa4output false)
92 | (plotreference true)
93 | (plotvalue false)
94 | (plotinvisibletext false)
95 | (padsonsilk false)
96 | (subtractmaskfromsilk false)
97 | (outputformat 1)
98 | (mirror false)
99 | (drillshape 0)
100 | (scaleselection 1)
101 | (outputdirectory "C:/Users/elisha3/Desktop/GitHub/Gerbers/SOT6/"))
102 | )
103 |
104 | (net 0 "")
105 | (net 1 "Net-(J1-Pad3)")
106 | (net 2 "Net-(J1-Pad2)")
107 | (net 3 "Net-(J1-Pad1)")
108 | (net 4 "Net-(J2-Pad3)")
109 | (net 5 "Net-(J2-Pad2)")
110 | (net 6 "Net-(J2-Pad1)")
111 |
112 | (net_class Default "This is the default net class."
113 | (clearance 0.2)
114 | (trace_width 0.25)
115 | (via_dia 0.8)
116 | (via_drill 0.4)
117 | (uvia_dia 0.3)
118 | (uvia_drill 0.1)
119 | (add_net "Net-(J1-Pad1)")
120 | (add_net "Net-(J1-Pad2)")
121 | (add_net "Net-(J1-Pad3)")
122 | (add_net "Net-(J2-Pad1)")
123 | (add_net "Net-(J2-Pad2)")
124 | (add_net "Net-(J2-Pad3)")
125 | )
126 |
127 | (module Package_TO_SOT_SMD:SOT-363_SC-70-6_Handsoldering (layer B.Cu) (tedit 5A02FF57) (tstamp 5D39DCA1)
128 | (at 65.532 36.83 270)
129 | (descr "SOT-363, SC-70-6, Handsoldering")
130 | (tags "SOT-363 SC-70-6 Handsoldering")
131 | (path /5D3AAC75)
132 | (attr smd)
133 | (fp_text reference J4 (at 0 2 90) (layer B.SilkS) hide
134 | (effects (font (size 1 1) (thickness 0.15)) (justify mirror))
135 | )
136 | (fp_text value Conn_02x03_Counter_Clockwise (at 0 -2 270) (layer B.Fab)
137 | (effects (font (size 1 1) (thickness 0.15)) (justify mirror))
138 | )
139 | (fp_line (start -0.175 1.1) (end -0.675 0.6) (layer B.Fab) (width 0.1))
140 | (fp_line (start 0.675 -1.1) (end -0.675 -1.1) (layer B.Fab) (width 0.1))
141 | (fp_line (start 0.675 1.1) (end 0.675 -1.1) (layer B.Fab) (width 0.1))
142 | (fp_line (start -0.675 0.6) (end -0.675 -1.1) (layer B.Fab) (width 0.1))
143 | (fp_line (start 0.675 1.1) (end -0.175 1.1) (layer B.Fab) (width 0.1))
144 | (fp_line (start -2.4 1.4) (end 2.4 1.4) (layer B.CrtYd) (width 0.05))
145 | (fp_line (start -2.4 1.4) (end -2.4 -1.4) (layer B.CrtYd) (width 0.05))
146 | (fp_line (start 2.4 -1.4) (end 2.4 1.4) (layer B.CrtYd) (width 0.05))
147 | (fp_line (start -0.7 -1.16) (end 0.7 -1.16) (layer B.SilkS) (width 0.12))
148 | (fp_line (start 0.7 1.16) (end -1.2 1.16) (layer B.SilkS) (width 0.12))
149 | (fp_line (start -2.4 -1.4) (end 2.4 -1.4) (layer B.CrtYd) (width 0.05))
150 | (fp_text user %R (at 0 0 180) (layer B.Fab)
151 | (effects (font (size 0.5 0.5) (thickness 0.075)) (justify mirror))
152 | )
153 | (pad 6 smd rect (at 1.33 0.65 270) (size 1.5 0.4) (layers B.Cu B.Paste B.Mask)
154 | (net 6 "Net-(J2-Pad1)"))
155 | (pad 5 smd rect (at 1.33 0 270) (size 1.5 0.4) (layers B.Cu B.Paste B.Mask)
156 | (net 5 "Net-(J2-Pad2)"))
157 | (pad 4 smd rect (at 1.33 -0.65 270) (size 1.5 0.4) (layers B.Cu B.Paste B.Mask)
158 | (net 4 "Net-(J2-Pad3)"))
159 | (pad 3 smd rect (at -1.33 -0.65 270) (size 1.5 0.4) (layers B.Cu B.Paste B.Mask)
160 | (net 1 "Net-(J1-Pad3)"))
161 | (pad 2 smd rect (at -1.33 0 270) (size 1.5 0.4) (layers B.Cu B.Paste B.Mask)
162 | (net 2 "Net-(J1-Pad2)"))
163 | (pad 1 smd rect (at -1.33 0.65 270) (size 1.5 0.4) (layers B.Cu B.Paste B.Mask)
164 | (net 3 "Net-(J1-Pad1)"))
165 | (model ${KISYS3DMOD}/Package_TO_SOT_SMD.3dshapes/SOT-363_SC-70-6.wrl
166 | (at (xyz 0 0 0))
167 | (scale (xyz 1 1 1))
168 | (rotate (xyz 0 0 0))
169 | )
170 | )
171 |
172 | (module Package_TO_SOT_SMD:SOT-23-6_Handsoldering (layer F.Cu) (tedit 5D7FE689) (tstamp 5D1DCDE4)
173 | (at 65.532 36.83 90)
174 | (descr "6-pin SOT-23 package, Handsoldering")
175 | (tags "SOT-23-6 Handsoldering")
176 | (path /5D1DC7E1)
177 | (attr smd)
178 | (fp_text reference J3 (at 0 -2.9 90) (layer F.SilkS) hide
179 | (effects (font (size 1 1) (thickness 0.15)))
180 | )
181 | (fp_text value Conn_02x03_Counter_Clockwise (at 0 2.9 90) (layer F.Fab)
182 | (effects (font (size 1 1) (thickness 0.15)))
183 | )
184 | (fp_line (start 0.9 -1.55) (end 0.9 1.55) (layer F.Fab) (width 0.1))
185 | (fp_line (start 0.9 1.55) (end -0.9 1.55) (layer F.Fab) (width 0.1))
186 | (fp_line (start -0.9 -0.9) (end -0.9 1.55) (layer F.Fab) (width 0.1))
187 | (fp_line (start 0.9 -1.55) (end -0.25 -1.55) (layer F.Fab) (width 0.1))
188 | (fp_line (start -0.9 -0.9) (end -0.25 -1.55) (layer F.Fab) (width 0.1))
189 | (fp_line (start -2.4 -1.8) (end 2.4 -1.8) (layer F.CrtYd) (width 0.05))
190 | (fp_line (start 2.4 -1.8) (end 2.4 1.8) (layer F.CrtYd) (width 0.05))
191 | (fp_line (start 2.4 1.8) (end -2.4 1.8) (layer F.CrtYd) (width 0.05))
192 | (fp_line (start -2.4 1.8) (end -2.4 -1.8) (layer F.CrtYd) (width 0.05))
193 | (fp_text user %R (at 0 0 180) (layer F.Fab)
194 | (effects (font (size 0.5 0.5) (thickness 0.075)))
195 | )
196 | (pad 5 smd rect (at 1.35 0 90) (size 1.56 0.65) (layers F.Cu F.Paste F.Mask)
197 | (net 2 "Net-(J1-Pad2)"))
198 | (pad 6 smd rect (at 1.35 -0.95 90) (size 1.56 0.65) (layers F.Cu F.Paste F.Mask)
199 | (net 3 "Net-(J1-Pad1)"))
200 | (pad 4 smd rect (at 1.35 0.95 90) (size 1.56 0.65) (layers F.Cu F.Paste F.Mask)
201 | (net 1 "Net-(J1-Pad3)"))
202 | (pad 3 smd rect (at -1.35 0.95 90) (size 1.56 0.65) (layers F.Cu F.Paste F.Mask)
203 | (net 4 "Net-(J2-Pad3)"))
204 | (pad 2 smd rect (at -1.35 0 90) (size 1.56 0.65) (layers F.Cu F.Paste F.Mask)
205 | (net 5 "Net-(J2-Pad2)"))
206 | (pad 1 smd rect (at -1.35 -0.95 90) (size 1.56 0.65) (layers F.Cu F.Paste F.Mask)
207 | (net 6 "Net-(J2-Pad1)"))
208 | (model ${KISYS3DMOD}/Package_TO_SOT_SMD.3dshapes/SOT-23-6.wrl
209 | (at (xyz 0 0 0))
210 | (scale (xyz 1 1 1))
211 | (rotate (xyz 0 0 0))
212 | )
213 | )
214 |
215 | (module logo:logo38x53 (layer B.Cu) (tedit 0) (tstamp 5D39DFB0)
216 | (at 53.4797 34.2646 180)
217 | (fp_text reference G*** (at 7.6073 4.1656 180) (layer B.SilkS) hide
218 | (effects (font (size 1.524 1.524) (thickness 0.3)) (justify mirror))
219 | )
220 | (fp_text value LOGO (at 5.4991 1.8542 180) (layer B.SilkS) hide
221 | (effects (font (size 1.524 1.524) (thickness 0.3)) (justify mirror))
222 | )
223 | (fp_poly (pts (xy 0.03556 -1.463463) (xy 0.035726 -1.496637) (xy 0.036193 -1.526026) (xy 0.036912 -1.550242)
224 | (xy 0.037834 -1.567896) (xy 0.038912 -1.5776) (xy 0.039602 -1.579033) (xy 0.044999 -1.575979)
225 | (xy 0.05768 -1.568455) (xy 0.07626 -1.557295) (xy 0.099355 -1.543329) (xy 0.12558 -1.52739)
226 | (xy 0.129772 -1.524836) (xy 0.164547 -1.503637) (xy 0.204212 -1.479456) (xy 0.244982 -1.454601)
227 | (xy 0.283071 -1.431379) (xy 0.30226 -1.419681) (xy 0.38862 -1.367029) (xy 0.389981 -1.486154)
228 | (xy 0.391343 -1.60528) (xy 0.67564 -1.60528) (xy 0.67564 -1.64592) (xy 0.39134 -1.64592)
229 | (xy 0.38862 -1.888666) (xy 0.304332 -1.837143) (xy 0.270924 -1.81672) (xy 0.232386 -1.793158)
230 | (xy 0.192255 -1.768619) (xy 0.154069 -1.745268) (xy 0.130755 -1.73101) (xy 0.103791 -1.714602)
231 | (xy 0.079807 -1.700167) (xy 0.060133 -1.688492) (xy 0.046101 -1.680366) (xy 0.039041 -1.676577)
232 | (xy 0.038513 -1.6764) (xy 0.037655 -1.681238) (xy 0.036895 -1.694786) (xy 0.036271 -1.715594)
233 | (xy 0.035822 -1.742212) (xy 0.035586 -1.77319) (xy 0.03556 -1.78816) (xy 0.03556 -1.89992)
234 | (xy -0.03029 -1.89992) (xy -0.031655 -1.785687) (xy -0.03302 -1.671455) (xy -0.084414 -1.703604)
235 | (xy -0.105139 -1.716487) (xy -0.123125 -1.727516) (xy -0.13639 -1.735486) (xy -0.142834 -1.739135)
236 | (xy -0.148873 -1.742599) (xy -0.162373 -1.750668) (xy -0.182143 -1.762619) (xy -0.206993 -1.777732)
237 | (xy -0.235731 -1.795283) (xy -0.267169 -1.81455) (xy -0.26924 -1.815822) (xy -0.38862 -1.889127)
238 | (xy -0.389981 -1.767523) (xy -0.391341 -1.64592) (xy -0.6858 -1.64592) (xy -0.6858 -1.60528)
239 | (xy -0.39116 -1.60528) (xy -0.39116 -1.4859) (xy -0.391077 -1.452291) (xy -0.390841 -1.422343)
240 | (xy -0.390479 -1.397459) (xy -0.390014 -1.379041) (xy -0.38947 -1.368493) (xy -0.389096 -1.36652)
241 | (xy -0.384257 -1.369031) (xy -0.3724 -1.375925) (xy -0.355144 -1.386239) (xy -0.334113 -1.399009)
242 | (xy -0.326866 -1.403448) (xy -0.300169 -1.419806) (xy -0.26825 -1.43932) (xy -0.234573 -1.459875)
243 | (xy -0.202602 -1.479355) (xy -0.19304 -1.485172) (xy -0.164302 -1.502671) (xy -0.134548 -1.520826)
244 | (xy -0.10659 -1.537921) (xy -0.083236 -1.552239) (xy -0.07493 -1.557347) (xy -0.03048 -1.584728)
245 | (xy -0.03048 -1.3462) (xy 0.03556 -1.3462) (xy 0.03556 -1.463463)) (layer B.SilkS) (width 0.01))
246 | (fp_poly (pts (xy 0.166599 -0.755905) (xy 0.189592 -0.762089) (xy 0.204203 -0.767395) (xy 0.219908 -0.774886)
247 | (xy 0.237784 -0.78528) (xy 0.258905 -0.799297) (xy 0.284348 -0.817657) (xy 0.315188 -0.841079)
248 | (xy 0.352502 -0.870283) (xy 0.358875 -0.875327) (xy 0.437308 -0.933874) (xy 0.51291 -0.982999)
249 | (xy 0.585595 -1.022657) (xy 0.655276 -1.052807) (xy 0.721869 -1.073404) (xy 0.771712 -1.082824)
250 | (xy 0.790517 -1.085485) (xy 0.802798 -1.087817) (xy 0.808033 -1.090061) (xy 0.805699 -1.092459)
251 | (xy 0.795273 -1.09525) (xy 0.776233 -1.098676) (xy 0.748055 -1.102977) (xy 0.71882 -1.107177)
252 | (xy 0.644548 -1.115637) (xy 0.570021 -1.120194) (xy 0.497361 -1.120859) (xy 0.428692 -1.117643)
253 | (xy 0.366137 -1.110559) (xy 0.335172 -1.105032) (xy 0.2644 -1.086545) (xy 0.20063 -1.061835)
254 | (xy 0.144212 -1.031133) (xy 0.095498 -0.994675) (xy 0.054838 -0.952693) (xy 0.022584 -0.90542)
255 | (xy 0.015727 -0.89251) (xy -0.003004 -0.855226) (xy -0.016807 -0.883543) (xy -0.047399 -0.934913)
256 | (xy -0.086097 -0.980228) (xy -0.132797 -1.019419) (xy -0.187398 -1.052419) (xy -0.249799 -1.079161)
257 | (xy -0.319895 -1.099579) (xy -0.356164 -1.107097) (xy -0.39262 -1.112355) (xy -0.43635 -1.116414)
258 | (xy -0.484528 -1.119169) (xy -0.534327 -1.120515) (xy -0.582922 -1.120346) (xy -0.627484 -1.118558)
259 | (xy -0.64008 -1.117655) (xy -0.658861 -1.115808) (xy -0.682586 -1.112987) (xy -0.70935 -1.109474)
260 | (xy -0.737246 -1.105553) (xy -0.764368 -1.101507) (xy -0.78881 -1.09762) (xy -0.808665 -1.094175)
261 | (xy -0.822026 -1.091455) (xy -0.826902 -1.089884) (xy -0.823025 -1.088745) (xy -0.811512 -1.087035)
262 | (xy -0.794845 -1.085122) (xy -0.794624 -1.085099) (xy -0.738782 -1.076177) (xy -0.681854 -1.06057)
263 | (xy -0.623202 -1.037952) (xy -0.562191 -1.007999) (xy -0.498182 -0.970386) (xy -0.430539 -0.924788)
264 | (xy -0.358623 -0.870879) (xy -0.3302 -0.848274) (xy -0.297567 -0.822403) (xy -0.270826 -0.802399)
265 | (xy -0.248383 -0.787259) (xy -0.228645 -0.775982) (xy -0.210018 -0.767564) (xy -0.191896 -0.761303)
266 | (xy -0.152127 -0.753996) (xy -0.114241 -0.756608) (xy -0.078736 -0.768993) (xy -0.046107 -0.791005)
267 | (xy -0.027367 -0.80962) (xy -0.003974 -0.836152) (xy 0.027223 -0.804981) (xy 0.050139 -0.784254)
268 | (xy 0.071355 -0.770212) (xy 0.08782 -0.762825) (xy 0.114875 -0.75475) (xy 0.139857 -0.752452)
269 | (xy 0.166599 -0.755905)) (layer B.SilkS) (width 0.01))
270 | (fp_poly (pts (xy 0.508885 0.228725) (xy 0.545984 0.224603) (xy 0.56042 0.22168) (xy 0.610789 0.205227)
271 | (xy 0.66004 0.181019) (xy 0.705491 0.150661) (xy 0.74446 0.115757) (xy 0.752941 0.106434)
272 | (xy 0.7747 0.081395) (xy 0.839389 0.081338) (xy 0.866081 0.081244) (xy 0.884683 0.080763)
273 | (xy 0.897266 0.079519) (xy 0.905901 0.07713) (xy 0.912658 0.073218) (xy 0.919608 0.067405)
274 | (xy 0.920669 0.066457) (xy 0.930968 0.055843) (xy 0.935852 0.045298) (xy 0.937234 0.030193)
275 | (xy 0.93726 0.026351) (xy 0.934111 0.004036) (xy 0.924223 -0.012333) (xy 0.906933 -0.023217)
276 | (xy 0.881578 -0.029074) (xy 0.855454 -0.030451) (xy 0.829528 -0.03048) (xy 0.835591 -0.05461)
277 | (xy 0.836896 -0.062744) (xy 0.837971 -0.076202) (xy 0.838821 -0.095551) (xy 0.839453 -0.121358)
278 | (xy 0.839872 -0.154192) (xy 0.840085 -0.194621) (xy 0.840096 -0.243213) (xy 0.839914 -0.300536)
279 | (xy 0.839542 -0.367159) (xy 0.83928 -0.40513) (xy 0.836906 -0.73152) (xy 0.812907 -0.73152)
280 | (xy 0.811583 -0.51181) (xy 0.81026 -0.2921) (xy 0.794225 -0.32004) (xy 0.760891 -0.368429)
281 | (xy 0.721116 -0.409719) (xy 0.675958 -0.443664) (xy 0.626475 -0.470019) (xy 0.573726 -0.488539)
282 | (xy 0.518768 -0.498978) (xy 0.462661 -0.50109) (xy 0.406463 -0.494631) (xy 0.351232 -0.479355)
283 | (xy 0.298027 -0.455017) (xy 0.27432 -0.440575) (xy 0.260856 -0.430343) (xy 0.243207 -0.41511)
284 | (xy 0.224116 -0.397306) (xy 0.212979 -0.386266) (xy 0.175149 -0.34066) (xy 0.145844 -0.289854)
285 | (xy 0.125378 -0.234634) (xy 0.114065 -0.175782) (xy 0.112245 -0.142227) (xy 0.203109 -0.142227)
286 | (xy 0.208199 -0.189431) (xy 0.22172 -0.235984) (xy 0.244019 -0.280849) (xy 0.271906 -0.318959)
287 | (xy 0.307115 -0.352268) (xy 0.348495 -0.378584) (xy 0.39437 -0.397362) (xy 0.443061 -0.40806)
288 | (xy 0.492891 -0.410133) (xy 0.53594 -0.404483) (xy 0.585828 -0.388603) (xy 0.630902 -0.364105)
289 | (xy 0.670386 -0.331696) (xy 0.703504 -0.292081) (xy 0.729479 -0.245965) (xy 0.74022 -0.218733)
290 | (xy 0.747681 -0.187539) (xy 0.751489 -0.150991) (xy 0.751554 -0.113186) (xy 0.747785 -0.078219)
291 | (xy 0.743735 -0.060612) (xy 0.724437 -0.012352) (xy 0.696855 0.030988) (xy 0.662048 0.068432)
292 | (xy 0.621078 0.099002) (xy 0.575004 0.121721) (xy 0.532344 0.134187) (xy 0.504674 0.1393)
293 | (xy 0.482604 0.141548) (xy 0.461991 0.140949) (xy 0.438692 0.137519) (xy 0.42418 0.134631)
294 | (xy 0.374338 0.119485) (xy 0.330146 0.096709) (xy 0.291952 0.067336) (xy 0.260103 0.032403)
295 | (xy 0.234947 -0.007056) (xy 0.21683 -0.050003) (xy 0.206102 -0.095406) (xy 0.203109 -0.142227)
296 | (xy 0.112245 -0.142227) (xy 0.11176 -0.133303) (xy 0.116667 -0.075132) (xy 0.130895 -0.019295)
297 | (xy 0.153702 0.033278) (xy 0.184346 0.081656) (xy 0.222085 0.12491) (xy 0.266178 0.162111)
298 | (xy 0.315881 0.192327) (xy 0.370455 0.214629) (xy 0.394747 0.221436) (xy 0.428948 0.227135)
299 | (xy 0.468434 0.229567) (xy 0.508885 0.228725)) (layer B.SilkS) (width 0.01))
300 | (fp_poly (pts (xy 0.176764 1.897837) (xy 0.343083 1.888246) (xy 0.507315 1.873075) (xy 0.667708 1.852341)
301 | (xy 0.69342 1.848428) (xy 0.723335 1.843817) (xy 0.744562 1.840361) (xy 0.758412 1.837431)
302 | (xy 0.766199 1.834399) (xy 0.769236 1.830637) (xy 0.768836 1.825515) (xy 0.766312 1.818407)
303 | (xy 0.764791 1.814273) (xy 0.756624 1.785249) (xy 0.749119 1.746511) (xy 0.742341 1.698488)
304 | (xy 0.736357 1.641608) (xy 0.73381 1.611604) (xy 0.731071 1.568192) (xy 0.728948 1.51604)
305 | (xy 0.727432 1.456552) (xy 0.726512 1.391129) (xy 0.726178 1.321172) (xy 0.72642 1.248083)
306 | (xy 0.727228 1.173264) (xy 0.728592 1.098116) (xy 0.730502 1.024043) (xy 0.732947 0.952444)
307 | (xy 0.735918 0.884722) (xy 0.739404 0.822279) (xy 0.739522 0.82042) (xy 0.742427 0.7747)
308 | (xy 0.833764 0.776386) (xy 0.896818 0.776166) (xy 0.950765 0.772953) (xy 0.996231 0.766671)
309 | (xy 1.03384 0.757244) (xy 1.053593 0.74974) (xy 1.081646 0.734624) (xy 1.100775 0.717175)
310 | (xy 1.112203 0.695561) (xy 1.117156 0.667951) (xy 1.1176 0.653947) (xy 1.117056 0.635233)
311 | (xy 1.114526 0.62255) (xy 1.10866 0.611787) (xy 1.100372 0.601463) (xy 1.083176 0.58463)
312 | (xy 1.061524 0.569558) (xy 1.034619 0.555993) (xy 1.001667 0.543684) (xy 0.96187 0.532377)
313 | (xy 0.914433 0.521819) (xy 0.85856 0.511758) (xy 0.793454 0.501942) (xy 0.781775 0.500329)
314 | (xy 0.680881 0.485055) (xy 0.585641 0.467346) (xy 0.491661 0.446281) (xy 0.394549 0.420939)
315 | (xy 0.37084 0.414276) (xy 0.353517 0.409102) (xy 0.327643 0.401034) (xy 0.294294 0.390426)
316 | (xy 0.254544 0.37763) (xy 0.20947 0.363) (xy 0.160148 0.346887) (xy 0.107654 0.329646)
317 | (xy 0.053063 0.311628) (xy -0.002549 0.293187) (xy -0.058106 0.274675) (xy -0.112531 0.256445)
318 | (xy -0.16475 0.238851) (xy -0.186689 0.231422) (xy -0.237815 0.214173) (xy -0.280741 0.199938)
319 | (xy -0.316837 0.188326) (xy -0.347476 0.178948) (xy -0.374029 0.171411) (xy -0.397868 0.165325)
320 | (xy -0.420364 0.1603) (xy -0.442889 0.155945) (xy -0.46482 0.152193) (xy -0.496099 0.148176)
321 | (xy -0.532792 0.145185) (xy -0.571915 0.143311) (xy -0.61048 0.142643) (xy -0.645503 0.143271)
322 | (xy -0.673996 0.145283) (xy -0.67818 0.145797) (xy -0.761317 0.160619) (xy -0.83668 0.181882)
323 | (xy -0.904399 0.209646) (xy -0.964604 0.243969) (xy -1.017428 0.284909) (xy -1.043713 0.310621)
324 | (xy -1.077977 0.351877) (xy -1.102786 0.393284) (xy -1.11876 0.436302) (xy -1.126518 0.482391)
325 | (xy -1.127512 0.508) (xy -1.123294 0.558654) (xy -1.110656 0.603627) (xy -1.089624 0.642888)
326 | (xy -1.060223 0.676404) (xy -1.022479 0.704145) (xy -0.976418 0.726077) (xy -0.944844 0.736423)
327 | (xy -0.90915 0.744283) (xy -0.866991 0.750078) (xy -0.82191 0.753514) (xy -0.777447 0.754297)
328 | (xy -0.74589 0.752911) (xy -0.69676 0.749113) (xy -0.693843 0.763696) (xy -0.687981 0.803753)
329 | (xy -0.684448 0.85307) (xy -0.683196 0.911031) (xy -0.684176 0.977019) (xy -0.687337 1.050417)
330 | (xy -0.692632 1.130608) (xy -0.70001 1.216976) (xy -0.709422 1.308904) (xy -0.72082 1.405774)
331 | (xy -0.734153 1.50697) (xy -0.749372 1.611876) (xy -0.766428 1.719874) (xy -0.773784 1.763974)
332 | (xy -0.778397 1.791883) (xy -0.78124 1.811366) (xy -0.782395 1.82399) (xy -0.781945 1.831317)
333 | (xy -0.779973 1.834912) (xy -0.777611 1.836081) (xy -0.767747 1.838256) (xy -0.749706 1.841588)
334 | (xy -0.725361 1.845765) (xy -0.696588 1.850477) (xy -0.665261 1.85541) (xy -0.633256 1.860253)
335 | (xy -0.62484 1.861491) (xy -0.474356 1.880058) (xy -0.317202 1.89296) (xy -0.15513 1.900211)
336 | (xy 0.01011 1.901832) (xy 0.176764 1.897837)) (layer B.SilkS) (width 0.01))
337 | )
338 |
339 | (module Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical (layer F.Cu) (tedit 5D398811) (tstamp 5D1DCDCE)
340 | (at 62.992 40.64 90)
341 | (descr "Through hole straight pin header, 1x03, 2.54mm pitch, single row")
342 | (tags "Through hole pin header THT 1x03 2.54mm single row")
343 | (path /5D1DD0C5)
344 | (fp_text reference J2 (at 0 -2.33 90) (layer F.SilkS) hide
345 | (effects (font (size 1 1) (thickness 0.15)))
346 | )
347 | (fp_text value Conn_01x03 (at 0 7.41 90) (layer F.Fab)
348 | (effects (font (size 1 1) (thickness 0.15)))
349 | )
350 | (fp_line (start 1.8 -1.8) (end -1.8 -1.8) (layer F.CrtYd) (width 0.05))
351 | (fp_line (start 1.8 6.85) (end 1.8 -1.8) (layer F.CrtYd) (width 0.05))
352 | (fp_line (start -1.8 6.85) (end 1.8 6.85) (layer F.CrtYd) (width 0.05))
353 | (fp_line (start -1.8 -1.8) (end -1.8 6.85) (layer F.CrtYd) (width 0.05))
354 | (fp_line (start -1.27 -0.635) (end -0.635 -1.27) (layer F.Fab) (width 0.1))
355 | (fp_line (start -1.27 6.35) (end -1.27 -0.635) (layer F.Fab) (width 0.1))
356 | (fp_line (start 1.27 6.35) (end -1.27 6.35) (layer F.Fab) (width 0.1))
357 | (fp_line (start 1.27 -1.27) (end 1.27 6.35) (layer F.Fab) (width 0.1))
358 | (fp_line (start -0.635 -1.27) (end 1.27 -1.27) (layer F.Fab) (width 0.1))
359 | (fp_text user %R (at 0 2.54) (layer F.Fab)
360 | (effects (font (size 1 1) (thickness 0.15)))
361 | )
362 | (pad 3 thru_hole oval (at 0 5.08 90) (size 1.7 1.7) (drill 1) (layers *.Cu *.Mask)
363 | (net 4 "Net-(J2-Pad3)"))
364 | (pad 2 thru_hole oval (at 0 2.54 90) (size 1.7 1.7) (drill 1) (layers *.Cu *.Mask)
365 | (net 5 "Net-(J2-Pad2)"))
366 | (pad 1 thru_hole circle (at 0 0 90) (size 1.7 1.7) (drill 1) (layers *.Cu *.Mask)
367 | (net 6 "Net-(J2-Pad1)"))
368 | (model ${KISYS3DMOD}/Connector_PinHeader_2.54mm.3dshapes/PinHeader_1x03_P2.54mm_Vertical.wrl
369 | (offset (xyz 0 0 -1.6))
370 | (scale (xyz 1 1 1))
371 | (rotate (xyz 0 180 0))
372 | )
373 | )
374 |
375 | (module Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical (layer F.Cu) (tedit 5D398804) (tstamp 5D1DCDB7)
376 | (at 62.992 33.02 90)
377 | (descr "Through hole straight pin header, 1x03, 2.54mm pitch, single row")
378 | (tags "Through hole pin header THT 1x03 2.54mm single row")
379 | (path /5D1DC59C)
380 | (fp_text reference J1 (at 0 -2.33 90) (layer F.SilkS) hide
381 | (effects (font (size 1 1) (thickness 0.15)))
382 | )
383 | (fp_text value Conn_01x03 (at 0 7.41 90) (layer F.Fab)
384 | (effects (font (size 1 1) (thickness 0.15)))
385 | )
386 | (fp_line (start 1.8 -1.8) (end -1.8 -1.8) (layer F.CrtYd) (width 0.05))
387 | (fp_line (start 1.8 6.85) (end 1.8 -1.8) (layer F.CrtYd) (width 0.05))
388 | (fp_line (start -1.8 6.85) (end 1.8 6.85) (layer F.CrtYd) (width 0.05))
389 | (fp_line (start -1.8 -1.8) (end -1.8 6.85) (layer F.CrtYd) (width 0.05))
390 | (fp_line (start -1.27 -0.635) (end -0.635 -1.27) (layer F.Fab) (width 0.1))
391 | (fp_line (start -1.27 6.35) (end -1.27 -0.635) (layer F.Fab) (width 0.1))
392 | (fp_line (start 1.27 6.35) (end -1.27 6.35) (layer F.Fab) (width 0.1))
393 | (fp_line (start 1.27 -1.27) (end 1.27 6.35) (layer F.Fab) (width 0.1))
394 | (fp_line (start -0.635 -1.27) (end 1.27 -1.27) (layer F.Fab) (width 0.1))
395 | (fp_text user %R (at 0 2.54) (layer F.Fab)
396 | (effects (font (size 1 1) (thickness 0.15)))
397 | )
398 | (pad 3 thru_hole oval (at 0 5.08 90) (size 1.7 1.7) (drill 1) (layers *.Cu *.Mask)
399 | (net 1 "Net-(J1-Pad3)"))
400 | (pad 2 thru_hole oval (at 0 2.54 90) (size 1.7 1.7) (drill 1) (layers *.Cu *.Mask)
401 | (net 2 "Net-(J1-Pad2)"))
402 | (pad 1 thru_hole circle (at 0 0 90) (size 1.7 1.7) (drill 1) (layers *.Cu *.Mask)
403 | (net 3 "Net-(J1-Pad1)"))
404 | (model ${KISYS3DMOD}/Connector_PinHeader_2.54mm.3dshapes/PinHeader_1x03_P2.54mm_Vertical.wrl
405 | (offset (xyz 0 0 -1.6))
406 | (scale (xyz 1 1 1))
407 | (rotate (xyz 0 180 0))
408 | )
409 | )
410 |
411 | (dimension 5.08 (width 0.15) (layer Dwgs.User)
412 | (gr_text "5.080 mm" (at 65.532 29.18) (layer Dwgs.User)
413 | (effects (font (size 1 1) (thickness 0.15)))
414 | )
415 | (feature1 (pts (xy 68.072 33.02) (xy 68.072 29.893579)))
416 | (feature2 (pts (xy 62.992 33.02) (xy 62.992 29.893579)))
417 | (crossbar (pts (xy 62.992 30.48) (xy 68.072 30.48)))
418 | (arrow1a (pts (xy 68.072 30.48) (xy 66.945496 31.066421)))
419 | (arrow1b (pts (xy 68.072 30.48) (xy 66.945496 29.893579)))
420 | (arrow2a (pts (xy 62.992 30.48) (xy 64.118504 31.066421)))
421 | (arrow2b (pts (xy 62.992 30.48) (xy 64.118504 29.893579)))
422 | )
423 | (dimension 7.62 (width 0.15) (layer Dwgs.User)
424 | (gr_text "7.620 mm" (at 73.055 36.83 270) (layer Dwgs.User)
425 | (effects (font (size 1 1) (thickness 0.15)))
426 | )
427 | (feature1 (pts (xy 68.072 40.64) (xy 72.341421 40.64)))
428 | (feature2 (pts (xy 68.072 33.02) (xy 72.341421 33.02)))
429 | (crossbar (pts (xy 71.755 33.02) (xy 71.755 40.64)))
430 | (arrow1a (pts (xy 71.755 40.64) (xy 71.168579 39.513496)))
431 | (arrow1b (pts (xy 71.755 40.64) (xy 72.341421 39.513496)))
432 | (arrow2a (pts (xy 71.755 33.02) (xy 71.168579 34.146504)))
433 | (arrow2b (pts (xy 71.755 33.02) (xy 72.341421 34.146504)))
434 | )
435 | (dimension 10.16 (width 0.15) (layer Dwgs.User)
436 | (gr_text "10.160 mm" (at 76.23 36.83 -90) (layer Dwgs.User)
437 | (effects (font (size 1 1) (thickness 0.15)))
438 | )
439 | (feature1 (pts (xy 68.072 31.75) (xy 75.516421 31.75)))
440 | (feature2 (pts (xy 68.072 41.91) (xy 75.516421 41.91)))
441 | (crossbar (pts (xy 74.93 41.91) (xy 74.93 31.75)))
442 | (arrow1a (pts (xy 74.93 31.75) (xy 75.516421 32.876504)))
443 | (arrow1b (pts (xy 74.93 31.75) (xy 74.343579 32.876504)))
444 | (arrow2a (pts (xy 74.93 41.91) (xy 75.516421 40.783496)))
445 | (arrow2b (pts (xy 74.93 41.91) (xy 74.343579 40.783496)))
446 | )
447 | (dimension 7.62 (width 0.15) (layer Dwgs.User)
448 | (gr_text "7.620 mm" (at 65.532 26.005) (layer Dwgs.User)
449 | (effects (font (size 1 1) (thickness 0.15)))
450 | )
451 | (feature1 (pts (xy 69.342 33.02) (xy 69.342 26.718579)))
452 | (feature2 (pts (xy 61.722 33.02) (xy 61.722 26.718579)))
453 | (crossbar (pts (xy 61.722 27.305) (xy 69.342 27.305)))
454 | (arrow1a (pts (xy 69.342 27.305) (xy 68.215496 27.891421)))
455 | (arrow1b (pts (xy 69.342 27.305) (xy 68.215496 26.718579)))
456 | (arrow2a (pts (xy 61.722 27.305) (xy 62.848504 27.891421)))
457 | (arrow2b (pts (xy 61.722 27.305) (xy 62.848504 26.718579)))
458 | )
459 | (gr_line (start 61.722 40.64) (end 61.722 33.02) (layer Edge.Cuts) (width 0.05) (tstamp 5E451B28))
460 | (gr_line (start 68.072 41.91) (end 62.992 41.91) (layer Edge.Cuts) (width 0.05) (tstamp 5E451B20))
461 | (gr_line (start 69.342 33.02) (end 69.342 40.64) (layer Edge.Cuts) (width 0.05) (tstamp 5E451B1E))
462 | (gr_line (start 62.992 31.75) (end 68.072 31.75) (layer Edge.Cuts) (width 0.05) (tstamp 5E451B1B))
463 | (gr_arc (start 62.992 33.02) (end 62.992 31.75) (angle -90) (layer Edge.Cuts) (width 0.05))
464 | (gr_arc (start 68.072 33.02) (end 69.342 33.02) (angle -90) (layer Edge.Cuts) (width 0.05))
465 | (gr_arc (start 68.072 40.64) (end 68.072 41.91) (angle -90) (layer Edge.Cuts) (width 0.05))
466 | (gr_arc (start 62.992 40.64) (end 61.722 40.64) (angle -90) (layer Edge.Cuts) (width 0.05))
467 | (gr_curve (pts (xy 16.3068 13.843) (xy 17.5895 14.5288) (xy 15.748 16.7894) (xy 14.9298 15.22)) (layer F.SilkS) (width 0.2))
468 | (gr_text 6 (at 62.992 34.671) (layer F.SilkS) (tstamp 5D4B4CFA)
469 | (effects (font (size 0.8 0.8) (thickness 0.13)))
470 | )
471 | (gr_text 4 (at 68.072 34.671) (layer F.SilkS) (tstamp 5D4B4CF7)
472 | (effects (font (size 0.8 0.8) (thickness 0.13)))
473 | )
474 | (gr_text 3 (at 68.072 39.116) (layer F.SilkS) (tstamp 5D4B4CF4)
475 | (effects (font (size 0.8 0.8) (thickness 0.13)))
476 | )
477 | (gr_text 1 (at 62.992 39.116) (layer F.SilkS)
478 | (effects (font (size 0.8 0.8) (thickness 0.13)))
479 | )
480 | (gr_text "SC-70\n0.65mm" (at 57.1754 38.7604 90) (layer B.SilkS) (tstamp 5D39DFC6)
481 | (effects (font (size 0.7 0.7) (thickness 0.11)) (justify mirror))
482 | )
483 | (gr_text "SOT23 0.95mm" (at 65.532 36.83) (layer F.SilkS)
484 | (effects (font (size 0.7 0.65) (thickness 0.11)))
485 | )
486 |
487 | (segment (start 68.072 34.222081) (end 68.072 33.02) (width 0.25) (layer F.Cu) (net 1))
488 | (segment (start 68.072 34.465) (end 68.072 34.222081) (width 0.25) (layer F.Cu) (net 1))
489 | (segment (start 67.057 35.48) (end 68.072 34.465) (width 0.25) (layer F.Cu) (net 1))
490 | (segment (start 66.482 35.48) (end 67.057 35.48) (width 0.25) (layer F.Cu) (net 1))
491 | (segment (start 66.182 34.91) (end 68.072 33.02) (width 0.25) (layer B.Cu) (net 1) (tstamp 5D39DFE6))
492 | (segment (start 66.182 35.88) (end 66.182 34.91) (width 0.25) (layer B.Cu) (net 1) (tstamp 5D39DFE7))
493 | (segment (start 65.532 35.48) (end 65.532 33.02) (width 0.25) (layer F.Cu) (net 2))
494 | (segment (start 65.532 35.88) (end 65.532 33.02) (width 0.25) (layer B.Cu) (net 2))
495 | (segment (start 62.992 34.12) (end 62.992 33.02) (width 0.25) (layer F.Cu) (net 3))
496 | (segment (start 62.992 34.465) (end 62.992 34.12) (width 0.25) (layer F.Cu) (net 3))
497 | (segment (start 64.007 35.48) (end 62.992 34.465) (width 0.25) (layer F.Cu) (net 3))
498 | (segment (start 64.582 35.48) (end 64.007 35.48) (width 0.25) (layer F.Cu) (net 3))
499 | (segment (start 64.882 34.91) (end 62.992 33.02) (width 0.25) (layer B.Cu) (net 3))
500 | (segment (start 64.882 35.88) (end 64.882 34.91) (width 0.25) (layer B.Cu) (net 3))
501 | (segment (start 68.072 39.437919) (end 68.072 40.64) (width 0.25) (layer F.Cu) (net 4))
502 | (segment (start 68.072 39.195) (end 68.072 39.437919) (width 0.25) (layer F.Cu) (net 4))
503 | (segment (start 67.057 38.18) (end 68.072 39.195) (width 0.25) (layer F.Cu) (net 4))
504 | (segment (start 66.482 38.18) (end 67.057 38.18) (width 0.25) (layer F.Cu) (net 4))
505 | (segment (start 66.182 38.75) (end 68.072 40.64) (width 0.25) (layer B.Cu) (net 4))
506 | (segment (start 66.182 37.78) (end 66.182 38.75) (width 0.25) (layer B.Cu) (net 4))
507 | (segment (start 65.532 38.18) (end 65.532 40.64) (width 0.25) (layer F.Cu) (net 5))
508 | (segment (start 65.532 37.78) (end 65.532 40.64) (width 0.25) (layer B.Cu) (net 5))
509 | (segment (start 62.992 39.54) (end 62.992 40.64) (width 0.25) (layer F.Cu) (net 6))
510 | (segment (start 62.992 39.195) (end 62.992 39.54) (width 0.25) (layer F.Cu) (net 6))
511 | (segment (start 64.007 38.18) (end 62.992 39.195) (width 0.25) (layer F.Cu) (net 6))
512 | (segment (start 64.582 38.18) (end 64.007 38.18) (width 0.25) (layer F.Cu) (net 6))
513 | (segment (start 64.882 38.75) (end 62.992 40.64) (width 0.25) (layer B.Cu) (net 6))
514 | (segment (start 64.882 37.78) (end 64.882 38.75) (width 0.25) (layer B.Cu) (net 6))
515 |
516 | )
517 |
--------------------------------------------------------------------------------
/tests/simple.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
75 |
--------------------------------------------------------------------------------