├── .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 | ![logo](docs/logo.png) 3 | 4 | # Stretch 5 | 6 | Allow your PCBs to _stretch_! 7 | 8 | ## The Process 9 | 10 | In KiCad: 11 | 12 | ![kicad1](docs/1.png) 13 | 14 | 15 | In Inkscape: 16 | 17 | 18 | ![inkscape1](docs/2.png) 19 | 20 | 21 | Modify: 22 | 23 | 24 | ![inkscape2](docs/3.png) 25 | 26 | 27 | Back to KiCad: 28 | 29 | 30 | ![kicad2](docs/4.png) 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-to-svg](Stretch/icons/to_svg.png) *(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-to-pcb](Stretch/icons/to_pcb.png) *(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 = ' 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 | 443 | 445 | 463 | 465 | 466 | 468 | image/svg+xml 469 | 471 | 472 | 473 | 474 | 475 | 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 = '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 = '' 23 | svg += str(body) 24 | svg += '' 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 += ' 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 = ' 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 = ' 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 = ' 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 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 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 61 | 67 | 68 | 74 | 75 | --------------------------------------------------------------------------------