├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── __init__.py
├── __main__.py
├── dialog.py
├── doc
├── fanout_tool.png
└── pcm.png
├── fanout.fbp
├── icon
├── icon_256x256.png
├── icon_512x512.png
└── icon_64x64.png
├── metadata.json.template
├── onekiwi
├── controller
│ ├── controller.py
│ ├── logtext.py
│ ├── package.json
│ ├── package.py
│ └── package.yaml
├── icon.png
├── image
│ ├── bothsides.png
│ ├── bothsides.svg
│ ├── bottomleft.png
│ ├── bottomleft.svg
│ ├── bottomright.png
│ ├── bottomright.svg
│ ├── coming.png
│ ├── counterclock.png
│ ├── counterclock.svg
│ ├── counterclockwise.png
│ ├── counterclockwise.svg
│ ├── horizontal.png
│ ├── horizontal.svg
│ ├── image.svg
│ ├── inside.png
│ ├── inside.svg
│ ├── outside.png
│ ├── outside.svg
│ ├── quadrant.png
│ ├── quadrant.svg
│ ├── topleft.png
│ ├── topleft.svg
│ ├── topright.png
│ ├── topright.svg
│ ├── vertical.png
│ └── vertical.svg
├── kicad
│ └── board.py
├── model
│ ├── bga.py
│ └── model.py
├── plugin.py
├── version.py
└── view
│ ├── dialog.py
│ └── view.py
└── release.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | *.pyc
3 | *.log
4 | fanout-tool-*.zip
5 | metadata.json
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 OneKiwi Technology Co., Ltd
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | @echo "Run test"
3 | python3 dialog.py
4 |
5 | release: release.sh
6 | @echo "Create release"
7 | ./release.sh
8 |
9 | install:
10 | @echo "Install Plugin"
11 | mkdir fanout-tool
12 | cp __init__.py fanout-tool/
13 | cp -r onekiwi/ fanout-tool/
14 | rm -rf ~/.local/share/kicad/7.0/scripting/plugins/fanout-tool/
15 | mv fanout-tool/ ~/.local/share/kicad/7.0/scripting/plugins
16 |
17 | uninstall:
18 | @echo "Uninstall Plugin"
19 | rm -rf ~/.local/share/kicad/7.0/scripting/plugins/fanout-tool/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #  Fanout Tool
2 |
3 | [](https://paypal.me/phutruong811)
4 |
5 | ##
6 |
7 |
8 |
9 | ## GUI
10 | 
11 |
12 | ## Installation 💾
13 |
14 | Add our custom repo to **the Plugin and Content Manager**, the URL is `https://raw.githubusercontent.com/OneKiwiTech/onekiwi-kicad-repository/main/repository.json`
15 |
16 | 
17 |
18 | From there you can install the plugin via the GUI.
19 |
20 |
21 | ## Demo Video
22 | [](https://youtu.be/-J81S3inhoc)
23 |
24 | ## Licence and credits
25 | Plugin code is licensed under MIT license, see LICENSE for more info.
26 | KiCad Plugin code/structure from:
27 | - [kicad-jlcpcb-tools](https://github.com/Bouni/kicad-jlcpcb-tools)
28 | - [wiki.wxpython.org](https://wiki.wxpython.org/ModelViewController)
29 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | from .onekiwi.plugin import FanoutAction # Note the relative import!
2 | FanoutAction().register() # Instantiate and register to Pcbnew
--------------------------------------------------------------------------------
/__main__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/__main__.py
--------------------------------------------------------------------------------
/dialog.py:
--------------------------------------------------------------------------------
1 | try:
2 | import pcbnew
3 | except:
4 | import sys
5 | sys.path.insert(0,"/usr/lib/python3.8/site-packages/")
6 | import pcbnew
7 | import wx
8 |
9 | from onekiwi.controller.controller import Controller
10 |
11 | filename = '/home/vanson/working/kicad/onekiwi/som-imx8qxp-fbga609/som-imx8qxp-fbga609.kicad_pcb'
12 |
13 | class SimplePluginApp(wx.App):
14 | def OnInit(self):
15 | try:
16 | board = pcbnew.LoadBoard(filename)
17 | controller = Controller(board)
18 | controller.Show()
19 | return True
20 | except OSError:
21 | print("OSError: Unable to open file for reading.")
22 | return 0
23 |
24 | def main():
25 | app = SimplePluginApp()
26 | app.MainLoop()
27 |
28 | print("Done")
29 |
30 | if __name__ == "__main__":
31 | main()
--------------------------------------------------------------------------------
/doc/fanout_tool.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/doc/fanout_tool.png
--------------------------------------------------------------------------------
/doc/pcm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/doc/pcm.png
--------------------------------------------------------------------------------
/icon/icon_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/icon/icon_256x256.png
--------------------------------------------------------------------------------
/icon/icon_512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/icon/icon_512x512.png
--------------------------------------------------------------------------------
/icon/icon_64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/icon/icon_64x64.png
--------------------------------------------------------------------------------
/metadata.json.template:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://go.kicad.org/pcm/schemas/v1",
3 | "name": "Fanout Tool",
4 | "description": "BGA Fanout",
5 | "description_full": "BGA fanout routing",
6 | "identifier": "vn.onekiwi.fanout-tool",
7 | "type": "plugin",
8 | "author": {
9 | "name": "OneKiwi",
10 | "contact": {
11 | "web": "https://github.com/OneKiwiTech"
12 | }
13 | },
14 | "license": "MIT",
15 | "resources": {
16 | "homepage": "https://github.com/OneKiwiTech/kicad-fanout-tool",
17 | "demo": "https://www.youtube.com/watch?v=-J81S3inhoc"
18 | },
19 | "versions": [
20 | {
21 | "version": "VERSION",
22 | "status": "stable",
23 | "kicad_version": "7.0",
24 | "download_sha256": "SHA256",
25 | "download_size": DOWNLOAD_SIZE,
26 | "download_url": "https://github.com/OneKiwiTech/kicad-fanout-tool/releases/download/VERSION/fanout-tool-VERSION.zip",
27 | "install_size": INSTALL_SIZE,
28 | "platforms": [
29 | "linux",
30 | "macos",
31 | "windows"
32 | ]
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/onekiwi/controller/controller.py:
--------------------------------------------------------------------------------
1 | from ..model.model import Model
2 | from ..view.view import FanoutView
3 | from .logtext import LogText
4 | import sys
5 | import logging
6 | import logging.config
7 | import wx
8 | import pcbnew
9 | from .package import get_packages
10 |
11 | class Controller:
12 | def __init__(self, board):
13 | self.view = FanoutView()
14 | self.board = board
15 | self.reference = None
16 | self.skip = None
17 | self.tracks = []
18 | self.vias = []
19 | self.packages = get_packages()
20 | self.logger = self.init_logger(self.view.textLog)
21 | self.model = Model(self.board, self.logger)
22 |
23 | # Connect Events
24 | self.view.buttonFanout.Bind(wx.EVT_BUTTON, self.OnButtonFanout)
25 | self.view.buttonUndo.Bind(wx.EVT_BUTTON, self.OnButtonUndo)
26 | self.view.buttonClear.Bind(wx.EVT_BUTTON, self.OnButtonClear)
27 | self.view.buttonClose.Bind(wx.EVT_BUTTON, self.OnButtonClose)
28 | self.view.choicePackage.Bind( wx.EVT_CHOICE, self.OnChoicePackage)
29 | self.view.choiceAlignment.Bind( wx.EVT_CHOICE, self.OnChoiceAlignment)
30 | self.view.choiceDirection.Bind( wx.EVT_CHOICE, self.OnChoiceDirection)
31 | self.view.editFiltter.Bind(wx.EVT_TEXT, self.OnFiltterChange)
32 | self.view.Bind(wx.EVT_CLOSE, self.OnClose) #解决进程驻留问题
33 |
34 | self.add_references()
35 | self.get_tracks_vias()
36 | self.set_package()
37 |
38 | def Show(self):
39 | self.view.Show()
40 |
41 | def Close(self):
42 | self.view.Destroy()
43 | def OnClose(self,event): #解决进程驻留问题
44 | self.view.Destroy()
45 | event.Skip()
46 | def OnButtonFanout(self, event):
47 | reference = self.view.GetReferenceSelected()
48 | if reference == '':
49 | self.logger.error('Please chose a Reference')
50 | return
51 | else:
52 | self.logger.info('Selected reference: %s' %reference)
53 |
54 | if len(self.tracks) > 0:
55 | track_index = self.view.GetTrackSelectedIndex()
56 | else:
57 | self.logger.error('Please add track width')
58 | return
59 | if len(self.tracks) > 0:
60 | via_index = self.view.GetViaSelectedIndex()
61 | else:
62 | self.logger.error('Please add via')
63 | return
64 | skip_index = self.view.GetSkipIndex()
65 | unused_pads = self.view.GetCheckUnusepad()
66 | package = self.view.GetPackageValue()
67 | self.logger.info('package: %s' %package)
68 | alignment = self.view.GetAlignmentValue()
69 | self.logger.info('alignment: %s' %alignment)
70 | if package == 'BGA' and alignment == 'Quadrant':
71 | direction = 'none'
72 | else:
73 | direction = self.view.GetDirectionValue()
74 | self.logger.info('direction: %s' %direction)
75 | self.model.update_data(reference,skip_index, self.tracks[track_index], self.vias[via_index],unused_pads)
76 | self.model.update_package(package, alignment, direction)
77 | self.model.fanout()
78 |
79 | def OnButtonUndo(self, event):
80 | self.model.remove_track_via()
81 |
82 | def OnButtonClear(self, event):
83 | self.view.textLog.SetValue('')
84 |
85 | def OnButtonClose(self, event):
86 | self.Close()
87 |
88 | def OnChoicePackage(self, event):
89 | index = event.GetEventObject().GetSelection()
90 | value = event.GetEventObject().GetString(index)
91 | package = self.packages[index]
92 | alignments = []
93 | directions = []
94 | for i, ali in enumerate(package.alignments, 0):
95 | alignments.append(ali.name)
96 | if i == 0:
97 | for direc in ali.directions:
98 | directions.append(direc.name)
99 | self.view.ClearAlignment()
100 | self.view.ClearDirection()
101 | if value == 'BGA staggered':
102 | alignments.clear()
103 | self.view.AddAlignment('none')
104 | if value == 'BGA':
105 | directions.clear()
106 | self.view.AddAlignment(alignments)
107 | self.view.AddDirection(directions)
108 | image = self.packages[index].alignments[0].directions[0].image
109 | self.view.SetImagePreview(image)
110 |
111 | def OnChoiceAlignment(self, event):
112 | x = self.view.GetPackageIndex()
113 | y = self.view.GetAlignmentIndex()
114 | value = self.view.GetAlignmentValue()
115 | directions = []
116 | direcs = self.packages[x].alignments[y].directions
117 | for direc in direcs:
118 | directions.append(direc.name)
119 | image = direcs[0].image
120 | self.view.ClearDirection()
121 | if value == 'Quadrant':
122 | directions.clear()
123 | self.view.AddDirection(directions)
124 | self.view.SetImagePreview(image)
125 |
126 | def OnChoiceDirection(self, event):
127 | x = self.view.GetPackageIndex()
128 | y = self.view.GetAlignmentIndex()
129 | i = event.GetEventObject().GetSelection()
130 | #value = event.GetEventObject().GetString(i)
131 | image = self.packages[x].alignments[y].directions[i].image
132 | self.view.SetImagePreview(image)
133 |
134 | def OnFiltterChange(self, event):
135 | self.logger.info('OnFiltterChange')
136 | value = event.GetEventObject().GetValue()
137 | self.logger.info('text: %s' %value)
138 | self.view.ClearReferences()
139 | for ref in self.model.references:
140 | if ref.rfind(value) != -1:
141 | self.view.AddReferences(ref)
142 | self.view.SetIndexReferences(0)
143 |
144 | def add_references(self):
145 | self.view.AddReferences(self.model.references)
146 | if self.model.indexSelected is not None:
147 | self.view.SetIndexReferences(self.model.indexSelected)
148 |
149 | def get_tracks_vias(self):
150 | units = pcbnew.GetUserUnits()
151 | unit = ''
152 | scale = 1
153 | # pcbnew.EDA_UNITS_INCHES = 0
154 | if units == pcbnew.EDA_UNITS_INCH:
155 | unit = 'in'
156 | scale = 25400000
157 | # pcbnew.EDA_UNITS_MILLIMETRES = 1
158 | elif units == pcbnew.EDA_UNITS_MM:
159 | unit = 'mm'
160 | scale = 1000000
161 | # pcbnew.EDA_UNITS_MILS = 5
162 | elif units == pcbnew.EDA_UNITS_MILS:
163 | unit = 'mil'
164 | scale = 25400
165 | else:
166 | unit = 'mil'
167 | scale = 25400
168 | tracks = self.board.GetDesignSettings().m_TrackWidthList
169 | vias = self.board.GetDesignSettings().m_ViasDimensionsList
170 | tracklist = []
171 | vialist = []
172 | for track in tracks:
173 | if track > 0:
174 | self.tracks.append(track)
175 | display = str(track/scale) + ' ' + unit
176 | tracklist.append(display)
177 | # pcbnew.VIA_DIMENSION
178 | for via in vias:
179 | if via.m_Diameter > 0:
180 | self.vias.append(via)
181 | diam = via.m_Diameter
182 | hole = via.m_Drill
183 | display = str(diam/scale) + ' / ' + str(hole/scale) + ' ' + unit
184 | vialist.append(display)
185 | self.view.AddTracksWidth(tracklist)
186 | self.view.AddViasSize(vialist)
187 | self.logger.info('get_design_settings')
188 |
189 | def set_package(self):
190 | default = 2 #bga
191 | packages = []
192 | alignments = []
193 | for package in self.packages:
194 | packages.append(package.name)
195 | if package.name == 'BGA':
196 | for alig in package.alignments:
197 | alignments.append(alig.name)
198 | self.view.AddPackageType(packages, default)
199 | self.view.AddAlignment(alignments)
200 | image = self.packages[default].alignments[0].directions[0].image
201 | self.view.SetImagePreview(image)
202 |
203 | def init_logger(self, texlog):
204 | root = logging.getLogger()
205 | root.setLevel(logging.DEBUG)
206 | # Log to stderr
207 | handler1 = logging.StreamHandler(sys.stderr)
208 | handler1.setLevel(logging.DEBUG)
209 | # and to our GUI
210 | handler2 = LogText(texlog)
211 | handler2.setLevel(logging.DEBUG)
212 | formatter = logging.Formatter(
213 | "%(asctime)s - %(levelname)s - %(funcName)s - %(message)s",
214 | datefmt="%Y.%m.%d %H:%M:%S",
215 | )
216 | handler1.setFormatter(formatter)
217 | handler2.setFormatter(formatter)
218 | root.addHandler(handler1)
219 | root.addHandler(handler2)
220 | return logging.getLogger(__name__)
221 |
--------------------------------------------------------------------------------
/onekiwi/controller/logtext.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | class LogText(logging.StreamHandler):
4 | def __init__(self, textctrl):
5 | logging.StreamHandler.__init__(self)
6 | self.textctrl = textctrl
7 |
8 | def emit(self, record):
9 | try:
10 | msg = self.format(record)
11 | self.textctrl.WriteText(msg + "\n")
12 | self.flush()
13 | except:
14 | pass
--------------------------------------------------------------------------------
/onekiwi/controller/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "package": [
3 | {
4 | "name": "SOIC",
5 | "alignment": [
6 | {
7 | "name": "Aligned, multiple rows",
8 | "direction": [
9 | {
10 | "name": "Inside",
11 | "image": "coming.png"
12 | },
13 | {
14 | "name": "Outside",
15 | "image": "coming.png"
16 | },
17 | {
18 | "name": "Both sides",
19 | "image": "coming.png"
20 | }
21 | ]
22 | },
23 | {
24 | "name": "Alternate, multiple rows",
25 | "direction": [
26 | {
27 | "name": "Inside",
28 | "image": "coming.png"
29 | },
30 | {
31 | "name": "Outside",
32 | "image": "coming.png"
33 | },
34 | {
35 | "name": "Both sides",
36 | "image": "coming.png"
37 | }
38 | ]
39 | },
40 | {
41 | "name": "Aligned, single row",
42 | "direction": [
43 | {
44 | "name": "Inside",
45 | "image": "coming.png"
46 | },
47 | {
48 | "name": "Outside",
49 | "image": "coming.png"
50 | },
51 | {
52 | "name": "Both sides",
53 | "image": "coming.png"
54 | }
55 | ]
56 | },
57 | {
58 | "name": "Alternate, single row",
59 | "direction": [
60 | {
61 | "name": "Inside",
62 | "image": "coming.png"
63 | },
64 | {
65 | "name": "Outside",
66 | "image": "coming.png"
67 | },
68 | {
69 | "name": "Both sides",
70 | "image": "coming.png"
71 | }
72 | ]
73 | }
74 | ]
75 | },
76 | {
77 | "name": "QUAD",
78 | "alignment": [
79 | {
80 | "name": "Aligned, multiple rows",
81 | "direction": [
82 | {
83 | "name": "Inside",
84 | "image": "coming.png"
85 | },
86 | {
87 | "name": "Outside",
88 | "image": "coming.png"
89 | },
90 | {
91 | "name": "Both sides",
92 | "image": "coming.png"
93 | }
94 | ]
95 | },
96 | {
97 | "name": "Alternate, multiple rows",
98 | "direction": [
99 | {
100 | "name": "Inside",
101 | "image": "coming.png"
102 | },
103 | {
104 | "name": "Outside",
105 | "image": "coming.png"
106 | },
107 | {
108 | "name": "Both sides",
109 | "image": "coming.png"
110 | }
111 | ]
112 | },
113 | {
114 | "name": "Aligned, single row",
115 | "direction": [
116 | {
117 | "name": "Inside",
118 | "image": "coming.png"
119 | },
120 | {
121 | "name": "Outside",
122 | "image": "coming.png"
123 | },
124 | {
125 | "name": "Both sides",
126 | "image": "coming.png"
127 | }
128 | ]
129 | },
130 | {
131 | "name": "Alternate, single row",
132 | "direction": [
133 | {
134 | "name": "Inside",
135 | "image": "coming.png"
136 | },
137 | {
138 | "name": "Outside",
139 | "image": "coming.png"
140 | },
141 | {
142 | "name": "Both sides",
143 | "image": "coming.png"
144 | }
145 | ]
146 | }
147 | ]
148 | },
149 | {
150 | "name": "BGA",
151 | "alignment": [
152 | {
153 | "name": "Quadrant",
154 | "direction": [
155 | {
156 | "name": "noname",
157 | "image": "quadrant.png"
158 | }
159 | ]
160 | },
161 | {
162 | "name": "Diagonal",
163 | "direction": [
164 | {
165 | "name": "TopLeft",
166 | "image": "topleft.png"
167 | },
168 | {
169 | "name": "TopRight",
170 | "image": "topright.png"
171 | },
172 | {
173 | "name": "BottomLeft",
174 | "image": "bottomleft.png"
175 | },
176 | {
177 | "name": "BottomRight",
178 | "image": "bottomright.png"
179 | }
180 | ]
181 | },
182 | {
183 | "name": "X-pattern",
184 | "direction": [
185 | {
186 | "name": "Counterclock",
187 | "image": "counterclock.png"
188 | },
189 | {
190 | "name": "Counterclockwise",
191 | "image": "counterclockwise.png"
192 | }
193 | ]
194 | }
195 | ]
196 | },
197 | {
198 | "name": "BGA staggered",
199 | "alignment": [
200 | {
201 | "name": "noname",
202 | "direction": [
203 | {
204 | "name": "Horizontal",
205 | "image": "coming.png"
206 | },
207 | {
208 | "name": "Vertical",
209 | "image": "coming.png"
210 | }
211 | ]
212 | }
213 | ]
214 | }
215 | ]
216 | }
--------------------------------------------------------------------------------
/onekiwi/controller/package.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | from typing import List
4 |
5 | class Direction:
6 | def __init__(self, name, image):
7 | self.name = name
8 | self.image = image
9 |
10 | class Alignment:
11 | def __init__(self, name, directions):
12 | self.name = name
13 | self.directions:List[Direction] = directions
14 |
15 | class Package:
16 | def __init__(self, name):
17 | self.name = name
18 | self.alignments:List[Alignment] = []
19 |
20 | class Packages:
21 | def __init__(self):
22 | self.data = None
23 | self.packages:List[Package] = []
24 | self.read_yaml()
25 | self.parser_data()
26 |
27 | def read_yaml(self):
28 | yaml_path = os.path.join(os.path.dirname(__file__), 'package.json') # Optional
29 | with open(yaml_path) as f:
30 | #self.data = yaml.load(f, Loader=yaml.FullLoader)
31 | self.data = json.load(f)
32 |
33 | def parser_data(self):
34 | for package in self.data['package']:
35 | name = package['name']
36 | pack = Package(name)
37 | for alignment in package['alignment']:
38 | ali = alignment['name']
39 | directions = []
40 | for direction in alignment['direction']:
41 | direc = Direction(direction['name'], direction['image'])
42 | directions.append(direc)
43 | align = Alignment(ali, directions)
44 | pack.alignments.append(align)
45 | self.packages.append(pack)
46 |
47 | def get_packages():
48 | packages = Packages()
49 | return packages.packages
--------------------------------------------------------------------------------
/onekiwi/controller/package.yaml:
--------------------------------------------------------------------------------
1 | package:
2 | - name: SOIC
3 | alignment:
4 | - name: Aligned, multiple rows
5 | direction:
6 | - name: Inside
7 | image: inside.svg
8 | - name: Outside
9 | image: outside.svg
10 | - name: Both sides
11 | image: bothsides.svg
12 | - name: Alternate, multiple rows
13 | direction:
14 | - name: Inside
15 | image: inside.svg
16 | - name: Outside
17 | image: outside.svg
18 | - name: Both sides
19 | image: bothsides.svg
20 | - name: Aligned, single row
21 | direction:
22 | - name: Inside
23 | image: inside.svg
24 | - name: Outside
25 | image: outside.svg
26 | - name: Both sides
27 | image: bothsides.svg
28 | - name: Alternate, single row
29 | direction:
30 | - name: Inside
31 | image: inside.svg
32 | - name: Outside
33 | image: outside.svg
34 | - name: Both sides
35 | image: bothsides.svg
36 | - name: QUAD
37 | alignment:
38 | - name: Aligned, multiple rows
39 | direction:
40 | - name: Inside
41 | image: inside.svg
42 | - name: Outside
43 | image: outside.svg
44 | - name: Both sides
45 | image: bothsides.svg
46 | - name: Alternate, multiple rows
47 | direction:
48 | - name: Inside
49 | image: inside.svg
50 | - name: Outside
51 | image: outside.svg
52 | - name: Both sides
53 | image: bothsides.svg
54 | - name: Aligned, single row
55 | direction:
56 | - name: Inside
57 | image: inside.svg
58 | - name: Outside
59 | image: outside.svg
60 | - name: Both sides
61 | image: bothsides.svg
62 | - name: Alternate, single row
63 | direction:
64 | - name: Inside
65 | image: inside.svg
66 | - name: Outside
67 | image: outside.svg
68 | - name: Both sides
69 | image: bothsides.svg
70 | - name: BGA
71 | alignment:
72 | - name: Quadrant
73 | direction:
74 | - name: noname
75 | image: quadrant.svg
76 | - name: Diagonal
77 | direction:
78 | - name: TopLeft
79 | image: topleft.svg
80 | - name: TopRight
81 | image: topright.svg
82 | - name: BottomLeft
83 | image: bottomleft.svg
84 | - name: BottomRight
85 | image: bottomright.svg
86 | - name: X-pattern
87 | direction:
88 | - name: Counterclock
89 | image: counterclock.svg
90 | - name: Counterclockwise
91 | image: counterclockwise.svg
92 | - name: BGA staggered
93 | alignment:
94 | - name: noname
95 | direction:
96 | - name: Horizontal
97 | image: horizontal.svg
98 | - name: Vertical
99 | image: vertical.svg
--------------------------------------------------------------------------------
/onekiwi/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/icon.png
--------------------------------------------------------------------------------
/onekiwi/image/bothsides.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/image/bothsides.png
--------------------------------------------------------------------------------
/onekiwi/image/bottomleft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/image/bottomleft.png
--------------------------------------------------------------------------------
/onekiwi/image/bottomleft.svg:
--------------------------------------------------------------------------------
1 |
151 |
--------------------------------------------------------------------------------
/onekiwi/image/bottomright.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/image/bottomright.png
--------------------------------------------------------------------------------
/onekiwi/image/bottomright.svg:
--------------------------------------------------------------------------------
1 |
151 |
--------------------------------------------------------------------------------
/onekiwi/image/coming.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/image/coming.png
--------------------------------------------------------------------------------
/onekiwi/image/counterclock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/image/counterclock.png
--------------------------------------------------------------------------------
/onekiwi/image/counterclock.svg:
--------------------------------------------------------------------------------
1 |
151 |
--------------------------------------------------------------------------------
/onekiwi/image/counterclockwise.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/image/counterclockwise.png
--------------------------------------------------------------------------------
/onekiwi/image/counterclockwise.svg:
--------------------------------------------------------------------------------
1 |
151 |
--------------------------------------------------------------------------------
/onekiwi/image/horizontal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/image/horizontal.png
--------------------------------------------------------------------------------
/onekiwi/image/horizontal.svg:
--------------------------------------------------------------------------------
1 |
79 |
--------------------------------------------------------------------------------
/onekiwi/image/image.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/onekiwi/image/inside.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/image/inside.png
--------------------------------------------------------------------------------
/onekiwi/image/inside.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/onekiwi/image/outside.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/image/outside.png
--------------------------------------------------------------------------------
/onekiwi/image/quadrant.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/image/quadrant.png
--------------------------------------------------------------------------------
/onekiwi/image/quadrant.svg:
--------------------------------------------------------------------------------
1 |
151 |
--------------------------------------------------------------------------------
/onekiwi/image/topleft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/image/topleft.png
--------------------------------------------------------------------------------
/onekiwi/image/topleft.svg:
--------------------------------------------------------------------------------
1 |
151 |
--------------------------------------------------------------------------------
/onekiwi/image/topright.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/image/topright.png
--------------------------------------------------------------------------------
/onekiwi/image/topright.svg:
--------------------------------------------------------------------------------
1 |
165 |
--------------------------------------------------------------------------------
/onekiwi/image/vertical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OneKiwiTech/kicad-fanout-tool/db8a28b7b2cfa6ee3c9092ee1eed9aa5a5783db2/onekiwi/image/vertical.png
--------------------------------------------------------------------------------
/onekiwi/image/vertical.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/onekiwi/kicad/board.py:
--------------------------------------------------------------------------------
1 | import pcbnew
2 | import os
3 | import re
4 | import wx
5 |
6 | PLUGIN_PATH = os.path.split(os.path.abspath(__file__))[0]
7 |
8 | def get_wxWidgets_version():
9 | v = re.search(r"wxWidgets\s([\d\.]+)", wx.version())
10 | v = int(v.group(1).replace(".", ""))
11 | return v
12 |
13 | def get_plugin_version():
14 | """READ Version from file"""
15 | if not os.path.isfile(os.path.join(PLUGIN_PATH, "VERSION")):
16 | return "unknown"
17 | with open(os.path.join(PLUGIN_PATH, "VERSION")) as f:
18 | return f.read()
19 |
20 | def get_kicad_build_version():
21 | return str(pcbnew.GetBuildVersion())
22 |
23 | def get_kicad_semantic_version():
24 | return str(pcbnew.GetSemanticVersion())
25 |
26 | def get_kicad_major_minor_version():
27 | return str(pcbnew.GetMajorMinorVersion())
28 |
29 | def get_kicad_version():
30 | version = str(pcbnew.Version())
31 | major = version.split(".")[0]
32 | minor = version.split(".")[1]
33 | patch = version.split(".")[2]
34 |
35 | def get_current_unit():
36 | unit = pcbnew.GetUserUnits()
37 | # pcbnew.EDA_UNITS_INCHES = 0
38 | if unit == pcbnew.EDA_UNITS_INCH:
39 | return 'in'
40 | # pcbnew.EDA_UNITS_MILLIMETRES = 1
41 | elif unit == pcbnew.EDA_UNITS_MM:
42 | return 'mm'
43 | # pcbnew.EDA_UNITS_MILS = 5
44 | elif unit == pcbnew.EDA_UNITS_MILS:
45 | return 'mil'
46 |
47 | def get_onekiwi_path():
48 | # controller dir
49 | current_path = os.path.dirname(__file__)
50 | onekiwi_path = os.path.dirname(current_path)
51 | return onekiwi_path
52 |
53 | def get_image_path():
54 | onekiwi_path = get_onekiwi_path()
55 | image_path = os.path.join(onekiwi_path, 'image')
56 | return image_path
57 |
--------------------------------------------------------------------------------
/onekiwi/model/bga.py:
--------------------------------------------------------------------------------
1 | import pcbnew
2 | import math
3 |
4 | class BGA:
5 | def __init__(self, board, reference,skip, track, via,unused_pads, alignment, direction, logger):
6 | self.logger = logger
7 | self.board = board
8 | self.reference = reference
9 | self.skip=skip
10 | self.track = track
11 | self.via = via
12 | self.unused_pads = unused_pads
13 | self.alignment = alignment
14 | self.direction = direction
15 | self.pitchx = 0
16 | self.pitchy = 0
17 | self.tracks = []
18 |
19 | self.group = pcbnew.PCB_GROUP(None)
20 |
21 | self.logger.info(reference)
22 | self.radian_pad = 0.0
23 | self.footprint = self.board.FindFootprintByReference(reference)
24 | self.radian = self.footprint.GetOrientation()
25 | self.degrees = self.footprint.GetOrientationDegrees()
26 | self.pads = self.footprint.Pads()
27 | self.x0 = self.footprint.GetPosition().x
28 | self.y0 = self.footprint.GetPosition().y
29 | self.init_data()
30 |
31 | def get_major_version(self):
32 | version = str(pcbnew.Version())
33 | major = int(version.split(".")[0])
34 | return major
35 |
36 | def init_data(self):
37 | if self.degrees not in [0.0 , 90.0, 180.0, -90.0]:
38 | degrees = self.degrees + 45.0
39 | self.footprint.SetOrientationDegrees(degrees)
40 | self.radian_pad = self.footprint.GetOrientation()
41 | self.footprint.SetOrientationDegrees(0)
42 | pos_x = []
43 | pos_y = []
44 |
45 | minx = self.pads[0].GetPosition().x
46 | maxx = self.pads[0].GetPosition().x
47 | miny = self.pads[0].GetPosition().y
48 | maxy = self.pads[0].GetPosition().y
49 |
50 | pos_x.append([self.pads[0].GetPosition()])
51 | pos_y.append([self.pads[0].GetPosition()])
52 |
53 | for pad in self.pads:
54 | pos = pad.GetPosition()
55 | if minx > pos.x:
56 | minx = pos.x
57 | if maxx < pos.x:
58 | maxx = pos.x
59 | if miny > pos.y:
60 | miny = pos.y
61 | if maxy < pos.y:
62 | maxy = pos.y
63 | checkx = True
64 | for arr in pos_x:
65 | if arr[0].y == pos.y and pos not in arr:
66 | checkx = False
67 | arr.append(pos)
68 | if checkx == True:
69 | pos_x.append([pos])
70 |
71 | checky = True
72 | for arr in pos_y:
73 | if arr[0].x == pos.x and pos not in arr:
74 | checky = False
75 | arr.append(pos)
76 | if checky == True:
77 | pos_y.append([pos])
78 |
79 | for arrs in pos_x:
80 | arrs.sort(key=lambda x:x.x)
81 | for arrs in pos_y:
82 | arrs.sort(key=lambda x:x.y)
83 |
84 | self.pitchx = pos_x[0][1].x - pos_x[0][0].x
85 | for arrs in pos_x:
86 | for i in range(len(arrs)):
87 | if i > 0:
88 | pitch = arrs[i].x - arrs[i-1].x
89 | if pitch > 0 and pitch < self.pitchx:
90 | self.pitchx = pitch
91 |
92 | self.pitchy = pos_y[0][1].y - pos_y[0][0].y
93 | for arrs in pos_y:
94 | for i in range(len(arrs)):
95 | if i > 0:
96 | pitch = arrs[i].y - arrs[i-1].y
97 | if pitch > 0 and pitch < self.pitchy:
98 | self.pitchy = pitch
99 | IU_PER_MM = 1000000
100 | px = round(self.pitchx/IU_PER_MM, 4)
101 | py = round(self.pitchy/IU_PER_MM, 4)
102 | if self.logger is not None:
103 | self.logger.info('pitch x: %f mm' %px)
104 | self.logger.info('pitch y: %f mm' %py)
105 | """
106 | for ind, arrs in enumerate(pos_y):
107 | self.logger.info('%d. sort---------------------' %ind)
108 | for i, arr in enumerate(arrs):
109 | self.logger.info('%d. %s' %(i, str(arr)))
110 | """
111 | self.footprint.SetOrientationDegrees(self.degrees)
112 | """
113 | if self.degrees in [0.0 , 90.0, 180.0, -90]:
114 | x = (minx + maxx)/2
115 | y = (miny + maxy)/2
116 | xstart = pcbnew.wxPoint(x, maxy)
117 | xend = pcbnew.wxPoint(x, miny)
118 | ystart = pcbnew.wxPoint(minx, y)
119 | yend = pcbnew.wxPoint(maxx, y)
120 | xtrack = pcbnew.PCB_TRACK(self.board)
121 | xtrack.SetStart(xstart)
122 | xtrack.SetEnd(xend)
123 | xtrack.SetWidth(self.track)
124 | xtrack.SetLayer(pcbnew.F_Cu)
125 | self.board.Add(xtrack)
126 |
127 | ytrack = pcbnew.PCB_TRACK(self.board)
128 | ytrack.SetStart(ystart)
129 | ytrack.SetEnd(yend)
130 | ytrack.SetWidth(self.track)
131 | ytrack.SetLayer(pcbnew.F_Cu)
132 | self.board.Add(ytrack)
133 | else:
134 | anphalx = (-1)*math.tan(self.radian)
135 | anphaly = 1/math.tan(self.radian)
136 | bx = self.y0 - anphalx*self.x0
137 | by = self.y0 - anphaly*self.x0
138 |
139 | # y = ax + b
140 | xyminx = anphalx*minx + bx
141 | xymaxx = anphalx*maxx + bx
142 | xstart = pcbnew.wxPoint(minx, xyminx)
143 | xend = pcbnew.wxPoint(maxx, xymaxx)
144 |
145 | yyminx = anphaly*minx + by
146 | yymaxx = anphaly*maxx + by
147 | ystart = pcbnew.wxPoint(minx, yyminx)
148 | yend = pcbnew.wxPoint(maxx, yymaxx)
149 |
150 | xtrack = pcbnew.PCB_TRACK(self.board)
151 | xtrack.SetStart(xstart)
152 | xtrack.SetEnd(xend)
153 | xtrack.SetWidth(self.track)
154 | xtrack.SetLayer(pcbnew.F_Cu)
155 | self.board.Add(xtrack)
156 |
157 | ytrack = pcbnew.PCB_TRACK(self.board)
158 | ytrack.SetStart(ystart)
159 | ytrack.SetEnd(yend)
160 | ytrack.SetWidth(self.track)
161 | ytrack.SetLayer(pcbnew.F_Cu)
162 | self.board.Add(ytrack)
163 | #######
164 | anx = -1*math.tan(self.radian_pad)
165 | any = 1/math.tan(self.radian_pad)
166 | b1 = self.y0 - anx*self.x0
167 | b2 = self.y0 - any*self.x0
168 | y1 = anx*minx + b1
169 | y2 = anx*maxx + b1
170 |
171 | y3 = any*minx + b2
172 | y4 = any*maxx + b2
173 | start1 = pcbnew.wxPoint(minx, y1)
174 | end1 = pcbnew.wxPoint(maxx, y2)
175 |
176 | start2 = pcbnew.wxPoint(minx, y3)
177 | end2 = pcbnew.wxPoint(maxx, y4)
178 |
179 | track1 = pcbnew.PCB_TRACK(self.board)
180 | track1.SetStart(start1)
181 | track1.SetEnd(end1)
182 | track1.SetWidth(self.track)
183 | track1.SetLayer(pcbnew.F_Cu)
184 | self.board.Add(track1)
185 |
186 | track2 = pcbnew.PCB_TRACK(self.board)
187 | track2.SetStart(start2)
188 | track2.SetEnd(end2)
189 | track2.SetWidth(self.track)
190 | track2.SetLayer(pcbnew.F_Cu)
191 | self.board.Add(track2)
192 | pcbnew.Refresh()
193 | """
194 | import math
195 |
196 | def get_xy_extremum(self, rotation_angle=0):
197 | pos_list = []
198 | for pad in self.pads:
199 | # 获取焊盘位置
200 | position = pad.GetPosition()
201 | # 计算旋转后的坐标
202 | rotated_x = position.x * math.cos(rotation_angle) - position.y * math.sin(rotation_angle)
203 | rotated_y = position.x * math.sin(rotation_angle) + position.y * math.cos(rotation_angle)
204 | pos_list.append((rotated_x, rotated_y)) # 存储为元组
205 |
206 | min_x = min(pos_list, key=lambda x: x[0])[0]
207 | max_x = max(pos_list, key=lambda x: x[0])[0]
208 | min_y = min(pos_list, key=lambda x: x[1])[1]
209 | max_y = max(pos_list, key=lambda x: x[1])[1]
210 |
211 | return min_x, max_x, min_y, max_y
212 |
213 | def skip_pads(self, pos, min_x, max_x, min_y, max_y, pad, rotation_angle=0):
214 | # 计算旋转坐标
215 | rotated_x = pos.x * math.cos(rotation_angle) - pos.y * math.sin(rotation_angle)
216 | rotated_y = pos.x * math.sin(rotation_angle) + pos.y * math.cos(rotation_angle)
217 |
218 | x_distance = min(rotated_x - min_x, max_x - rotated_x)
219 | y_distance = min(rotated_y - min_y, max_y - rotated_y)
220 | n_ring = min(x_distance, y_distance) / self.pitchx
221 | net_name = pad.GetNet().GetNetname()
222 |
223 | if n_ring < self.skip or (("unconnected" in net_name) and not self.unused_pads):
224 | return True
225 | else:
226 | return False
227 |
228 |
229 | def fanout(self):
230 | if self.alignment == 'Quadrant':
231 | if self.degrees in [0.0 , 90.0, 180.0, -90.0]:
232 | self.quadrant_0_90_180()
233 | elif self.degrees in [45.0 , 135.0, -135.0, -45.0]:
234 | self.quadrant_45_135()
235 | else:
236 | self.quadrant_other_angle()
237 | elif self.alignment == 'Diagonal':
238 | if self.degrees in [0.0 , 90.0, 180.0, -90.0]:
239 | self.diagonal_0_90_180()
240 | elif self.degrees in [45.0 , 135.0, -135.0, -45.0]:
241 | self.diagonal_45_135()
242 | else:
243 | self.diagonal_other_angle()
244 | elif self.alignment == 'X-pattern':
245 | if self.degrees in [0.0 , 90.0, 180.0, -90.0]:
246 | self.xpattern_0_90_180()
247 | elif self.degrees in [45.0 , 135.0, -135.0, -45.0]:
248 | self.xpattern_45_135()
249 | else:
250 | self.xpattern_other_angle()
251 |
252 | pcbnew.Refresh()
253 |
254 | # quadrant
255 | def quadrant_0_90_180(self):
256 | min_x, max_x, min_y, max_y = self.get_xy_extremum()
257 | for pad in self.pads:
258 | pos = pad.GetPosition()
259 | net = pad.GetNetCode()
260 | if self.skip_pads(pos, min_x, max_x, min_y, max_y,pad):
261 | continue
262 | if pos.y > self.y0:
263 | if pos.x > self.x0:
264 | # bottom-right 225
265 | x = pos.x + self.pitchx/2
266 | y = pos.y + self.pitchy/2
267 | else:
268 | # bottom-left 135
269 | x = pos.x - self.pitchx/2
270 | y = pos.y + self.pitchy/2
271 | point = pcbnew.wxPoint(x, y)
272 | end = pcbnew.VECTOR2I(point.x, point.y)
273 | self.add_track(net, pos, end)
274 | self.add_via(net, end)
275 | else:
276 | if pos.x > self.x0:
277 | # top-right 315
278 | x = pos.x + self.pitchx/2
279 | y = pos.y - self.pitchy/2
280 | else:
281 | # top-left 45
282 | x = pos.x - self.pitchx/2
283 | y = pos.y - self.pitchy/2
284 | point = pcbnew.wxPoint(x, y)
285 | end = pcbnew.VECTOR2I(point.x, point.y)
286 | self.add_track(net, pos, end)
287 | self.add_via(net, end)
288 |
289 | def quadrant_45_135(self):
290 | bx = self.y0 + self.x0
291 | by = self.y0 - self.x0
292 | pitch = math.sqrt(self.pitchx*self.pitchx + self.pitchy*self.pitchy)/2
293 | min_x, max_x, min_y, max_y = self.get_xy_extremum(45/180*math.pi)
294 | for pad in self.pads:
295 | pos = pad.GetPosition()
296 | net = pad.GetNetCode()
297 | if self.skip_pads(pos, min_x, max_x, min_y, max_y,pad, 45/180*math.pi):
298 | continue
299 | y1 = bx - pos.x
300 | y2 = by + pos.x
301 | if pos.y > y1:
302 | if pos.y > y2:
303 | # bottom
304 | x = pos.x
305 | y = pos.y + pitch
306 | else:
307 | # left
308 | x = pos.x + pitch
309 | y = pos.y
310 | #end = pcbnew.wxPoint(x, y)
311 | point = pcbnew.wxPoint(x, y)
312 | end = pcbnew.VECTOR2I(point.x, point.y)
313 | self.add_track(net, pos, end)
314 | self.add_via(net, end)
315 | else:
316 | if pos.y > y2:
317 | # right
318 | x = pos.x - pitch
319 | y = pos.y
320 | else:
321 | # top
322 | x = pos.x
323 | y = pos.y - pitch
324 | #end = pcbnew.wxPoint(x, y)
325 | point = pcbnew.wxPoint(x, y)
326 | end = pcbnew.VECTOR2I(point.x, point.y)
327 | self.add_track(net, pos, end)
328 | self.add_via(net, end)
329 |
330 | def quadrant_other_angle(self):
331 | #anphalx = (-1)*math.tan(self.radian)
332 | #anphaly = 1/math.tan(self.radian)
333 | anphalx = (-1)*self.radian.Tan()
334 | anphaly = 1/self.radian.Tan()
335 | bx0 = self.y0 - anphalx*self.x0
336 | by0 = self.y0 - anphaly*self.x0
337 |
338 | #pax = -1*math.tan(self.radian_pad)
339 | #pay = 1/math.tan(self.radian_pad)
340 | pax = -1*self.radian_pad.Tan()
341 | pay = 1/self.radian_pad.Tan()
342 | pitch = math.sqrt(self.pitchx*self.pitchx + self.pitchy*self.pitchy)/2
343 | angle_radian = self.degrees/180*math.pi
344 | min_x, max_x, min_y, max_y = self.get_xy_extremum(angle_radian)
345 | for pad in self.pads:
346 | pos = pad.GetPosition()
347 | net = pad.GetNetCode()
348 | if self.skip_pads(pos, min_x, max_x, min_y, max_y,pad, angle_radian):
349 | continue
350 | y1 = anphalx*pos.x + bx0
351 | y2 = anphaly*pos.x + by0
352 | pbx = pos.y - pax*pos.x
353 | pby = pos.y - pay*pos.x
354 |
355 | # d^2 = (x - x0)^2 + (y - y0)^2
356 | # = (x - x0)^2 + (a.x + b - y0)^2
357 | # = x^2 - 2x.x0 + x0^2 + a^2.x^2 + a.b.x - a.y0.x + a.b.x + b^2 - b.y0 - a.y0.x - b.y0 + y0^2
358 | # = (1 + a.a)x.x = (-2.x0 + 2.a.b - 2.a.y0)x + (x0.x0 + b.b - 2.b.y0 + y0.y0) - d.d
359 | ax = pax*pax + 1
360 | bx = 2*pax*pbx - 2*pos.x - 2*pax*pos.y
361 | cx = pos.x*pos.x + pbx*pbx + pos.y*pos.y - 2*pbx*pos.y - pitch*pitch
362 |
363 | ay = pay*pay + 1
364 | by = 2*pay*pby - 2*pos.x - 2*pay*pos.y
365 | cy = pos.x*pos.x + pby*pby + pos.y*pos.y - 2*pby*pos.y - pitch*pitch
366 |
367 | deltax = bx*bx - 4*ax*cx
368 | deltay = by*by - 4*ay*cy
369 | if deltax > 0:
370 | x1 = (-(bx) + math.sqrt(deltax))/(2*ax)
371 | x2 = (-(bx) - math.sqrt(deltax))/(2*ax)
372 | if deltay > 0:
373 | x3 = (-(by) + math.sqrt(deltay))/(2*ay)
374 | x4 = (-(by) - math.sqrt(deltay))/(2*ay)
375 | degrees_0to45 = self.degrees > 0 and self.degrees < 45
376 | degrees_45to90 = self.degrees > 45 and self.degrees < 90
377 | degrees_90to135 = self.degrees > 90 and self.degrees < 135
378 | degrees_135to180 =self.degrees > 135 and self.degrees < 180
379 | degrees_0to90 = self.degrees > 0 and self.degrees < 90
380 | degrees_90to180 =self.degrees > 90 and self.degrees < 180
381 |
382 | degrees_n45to0 = self.degrees > -45 and self.degrees < 0
383 | degrees_n90to45 = self.degrees > -90 and self.degrees < -45
384 | degrees_n135to90 = self.degrees > -135 and self.degrees < -90
385 | degrees_n180to135 =self.degrees > -180 and self.degrees < -135
386 | degrees_n180to90 =self.degrees > -180 and self.degrees < -90
387 | degrees_n90to0 = self.degrees > -90 and self.degrees < 0
388 | if pos.y > y1:
389 | x = 0
390 | y = 0
391 | if pos.y > y2:
392 | # bottom-left
393 | if degrees_0to45 or degrees_n180to135:
394 | x = x2
395 | y = pax*x + pbx
396 | elif degrees_45to90 or degrees_n135to90:
397 | x = x1
398 | y = pax*x + pbx
399 | elif degrees_90to135 or degrees_n90to45:
400 | x = x4
401 | y = pay*x + pby
402 | elif degrees_135to180 or degrees_n45to0:
403 | x = x3
404 | y = pay*x + pby
405 |
406 | else:
407 | # bottom-right
408 | if degrees_0to90 or degrees_n180to90:
409 | x = x3
410 | y = pay*x + pby
411 | elif degrees_90to180 or degrees_n90to0:
412 | x = x2
413 | y = pax*x + pbx
414 | #end = pcbnew.wxPoint(x, y)
415 | point = pcbnew.wxPoint(x, y)
416 | end = pcbnew.VECTOR2I(point.x, point.y)
417 | self.add_track(net, pos, end)
418 | self.add_via(net, end)
419 | else:
420 | x = 0
421 | y = 0
422 | if pos.y > y2:
423 | # top-left
424 | if degrees_0to90 or degrees_n180to90:
425 | x = x4
426 | y = pay*x + pby
427 | elif degrees_90to180 or degrees_n90to0:
428 | x = x1
429 | y = pax*x + pbx
430 | else:
431 | # bottom-right
432 | if degrees_0to45 or degrees_n180to135:
433 | x = x1
434 | y = pax*x + pbx
435 | elif degrees_45to90 or degrees_n135to90:
436 | x = x2
437 | y = pax*x + pbx
438 | elif degrees_90to135 or degrees_n90to45:
439 | x = x3
440 | y = pay*x + pby
441 | elif degrees_135to180 or degrees_n45to0:
442 | x = x4
443 | y = pay*x + pby
444 | #end = pcbnew.wxPoint(x, y)
445 | point = pcbnew.wxPoint(x, y)
446 | end = pcbnew.VECTOR2I(point.x, point.y)
447 | self.add_track(net, pos, end)
448 | self.add_via(net, end)
449 |
450 | # diagonal
451 | def diagonal_0_90_180(self):
452 | min_x, max_x, min_y, max_y = self.get_xy_extremum()
453 | for pad in self.pads:
454 | pos = pad.GetPosition()
455 | net = pad.GetNetCode()
456 | if self.skip_pads(pos, min_x, max_x, min_y, max_y,pad):
457 | continue
458 | x = 0
459 | y = 0
460 | if self.direction =='TopLeft':
461 | x = pos.x - self.pitchx/2
462 | y = pos.y - self.pitchy/2
463 | if self.direction =='TopRight':
464 | x = pos.x + self.pitchx/2
465 | y = pos.y - self.pitchy/2
466 | if self.direction =='BottomLeft':
467 | x = pos.x - self.pitchx/2
468 | y = pos.y + self.pitchy/2
469 | if self.direction =='BottomRight':
470 | x = pos.x + self.pitchx/2
471 | y = pos.y + self.pitchy/2
472 | #end = pcbnew.wxPoint(x, y)
473 | point = pcbnew.wxPoint(x, y)
474 | end = pcbnew.VECTOR2I(point.x, point.y)
475 | self.add_track(net, pos, end)
476 | self.add_via(net, end)
477 |
478 | def diagonal_45_135(self):
479 | pitch = math.sqrt(self.pitchx*self.pitchx + self.pitchy*self.pitchy)/2
480 | angle_radian = 45/180*math.pi
481 | min_x, max_x, min_y, max_y = self.get_xy_extremum(angle_radian)
482 | for pad in self.pads:
483 | pos = pad.GetPosition()
484 | net = pad.GetNetCode()
485 | if self.skip_pads(pos, min_x, max_x, min_y, max_y,pad, angle_radian):
486 | continue
487 | x = pos.x
488 | y = pos.y
489 | if self.direction =='TopLeft':
490 | x = pos.x - pitch
491 | y = pos.y
492 | if self.direction =='TopRight':
493 | x = pos.x + pitch
494 | y = pos.y
495 | if self.direction =='BottomLeft':
496 | x = pos.x
497 | y = pos.y + pitch
498 | if self.direction =='BottomRight':
499 | x = pos.x
500 | y = pos.y - pitch
501 | #end = pcbnew.wxPoint(x, y)
502 | point = pcbnew.wxPoint(x, y)
503 | end = pcbnew.VECTOR2I(point.x, point.y)
504 | self.add_track(net, pos, end)
505 | self.add_via(net, end)
506 |
507 | def diagonal_other_angle(self):
508 | #pax = -1*math.tan(self.radian_pad)
509 | #pay = 1/math.tan(self.radian_pad)
510 | pax = (-1)*self.radian_pad.Tan()
511 | pay = 1/self.radian_pad.Tan()
512 | pitch = math.sqrt(self.pitchx*self.pitchx + self.pitchy*self.pitchy)/2
513 | angle_radian = self.degrees/180*math.pi
514 | min_x, max_x, min_y, max_y = self.get_xy_extremum(angle_radian)
515 | for pad in self.pads:
516 | pos = pad.GetPosition()
517 | net = pad.GetNetCode()
518 | if self.skip_pads(pos, min_x, max_x, min_y, max_y,pad, angle_radian):
519 | continue
520 | pbx = pos.y - pax*pos.x
521 | pby = pos.y - pay*pos.x
522 |
523 | # d^2 = (x - x0)^2 + (y - y0)^2
524 | # = (x - x0)^2 + (a.x + b - y0)^2
525 | # = x^2 - 2x.x0 + x0^2 + a^2.x^2 + a.b.x - a.y0.x + a.b.x + b^2 - b.y0 - a.y0.x - b.y0 + y0^2
526 | # = (1 + a.a)x.x = (-2.x0 + 2.a.b - 2.a.y0)x + (x0.x0 + b.b - 2.b.y0 + y0.y0) - d.d
527 | ax = pax*pax + 1
528 | bx = 2*pax*pbx - 2*pos.x - 2*pax*pos.y
529 | cx = pos.x*pos.x + pbx*pbx + pos.y*pos.y - 2*pbx*pos.y - pitch*pitch
530 |
531 | ay = pay*pay + 1
532 | by = 2*pay*pby - 2*pos.x - 2*pay*pos.y
533 | cy = pos.x*pos.x + pby*pby + pos.y*pos.y - 2*pby*pos.y - pitch*pitch
534 |
535 | deltax = bx*bx - 4*ax*cx
536 | deltay = by*by - 4*ay*cy
537 | if deltax > 0:
538 | x1 = (-(bx) + math.sqrt(deltax))/(2*ax)
539 | x2 = (-(bx) - math.sqrt(deltax))/(2*ax)
540 | if deltay > 0:
541 | x3 = (-(by) + math.sqrt(deltay))/(2*ay)
542 | x4 = (-(by) - math.sqrt(deltay))/(2*ay)
543 | x = pos.x
544 | y = pos.y
545 | if self.direction =='TopLeft':
546 | x = x4
547 | y = pay*x + pby
548 | if self.direction =='TopRight':
549 | x = x2
550 | y = pax*x + pbx
551 | if self.direction =='BottomLeft':
552 | x = x1
553 | y = pax*x + pbx
554 | if self.direction =='BottomRight':
555 | x = x3
556 | y = pay*x + pby
557 | #end = pcbnew.wxPoint(x, y)
558 | point = pcbnew.wxPoint(x, y)
559 | end = pcbnew.VECTOR2I(point.x, point.y)
560 | self.add_track(net, pos, end)
561 | self.add_via(net, end)
562 |
563 | #X-pattern
564 | def xpattern_0_90_180(self):
565 | bx = self.y0 + self.x0
566 | by = self.y0 - self.x0
567 | min_x, max_x, min_y, max_y = self.get_xy_extremum()
568 | for pad in self.pads:
569 | pos = pad.GetPosition()
570 | net = pad.GetNetCode()
571 | if self.skip_pads(pos, min_x, max_x, min_y, max_y,pad):
572 | continue
573 | y1 = bx - pos.x
574 | y2 = by + pos.x
575 | x = 0
576 | y = 0
577 | if pos.y > y1:
578 | if pos.y > y2:
579 | #bottom
580 | if self.direction =='Counterclock':
581 | x = pos.x - self.pitchx/2
582 | y = pos.y + self.pitchy/2
583 | if self.direction =='Counterclockwise':
584 | x = pos.x + self.pitchx/2
585 | y = pos.y + self.pitchy/2
586 | else:
587 | #right
588 | if self.direction =='Counterclock':
589 | x = pos.x + self.pitchx/2
590 | y = pos.y + self.pitchy/2
591 | if self.direction =='Counterclockwise':
592 | x = pos.x + self.pitchx/2
593 | y = pos.y - self.pitchy/2
594 | else:
595 | if pos.y > y2:
596 | #left
597 | if self.direction =='Counterclock':
598 | x = pos.x - self.pitchx/2
599 | y = pos.y - self.pitchy/2
600 | if self.direction =='Counterclockwise':
601 | x = pos.x - self.pitchx/2
602 | y = pos.y + self.pitchy/2
603 | else:
604 | #top
605 | if self.direction =='Counterclock':
606 | x = pos.x + self.pitchx/2
607 | y = pos.y - self.pitchy/2
608 | if self.direction =='Counterclockwise':
609 | x = pos.x - self.pitchx/2
610 | y = pos.y - self.pitchy/2
611 | #end = pcbnew.wxPoint(x, y)
612 | point = pcbnew.wxPoint(x, y)
613 | end = pcbnew.VECTOR2I(point.x, point.y)
614 | self.add_track(net, pos, end)
615 | self.add_via(net, end)
616 |
617 | def xpattern_45_135(self):
618 | pitch = math.sqrt(self.pitchx*self.pitchx + self.pitchy*self.pitchy)/2
619 | angle_radian = 45/180*math.pi
620 | min_x, max_x, min_y, max_y = self.get_xy_extremum(angle_radian)
621 | for pad in self.pads:
622 | pos = pad.GetPosition()
623 | net = pad.GetNetCode()
624 | if self.skip_pads(pos, min_x, max_x, min_y, max_y,pad,angle_radian):
625 | continue
626 | x = 0
627 | y = 0
628 | if pos.y > self.y0:
629 | if pos.x > self.x0:
630 | #bottom-right
631 | if self.direction =='Counterclock':
632 | x = pos.x
633 | y = pos.y + pitch
634 | if self.direction =='Counterclockwise':
635 | x = pos.x + pitch
636 | y = pos.y
637 | else:
638 | #bottom-left
639 | if self.direction =='Counterclock':
640 | x = pos.x - pitch
641 | y = pos.y
642 | if self.direction =='Counterclockwise':
643 | x = pos.x
644 | y = pos.y + pitch
645 | else:
646 | if pos.x > self.x0:
647 | #bottom-right
648 | if self.direction =='Counterclock':
649 | x = pos.x + pitch
650 | y = pos.y
651 | if self.direction =='Counterclockwise':
652 | x = pos.x
653 | y = pos.y - pitch
654 | else:
655 | #bottom-left
656 | if self.direction =='Counterclock':
657 | x = pos.x
658 | y = pos.y - pitch
659 | if self.direction =='Counterclockwise':
660 | x = pos.x - pitch
661 | y = pos.y
662 |
663 | #end = pcbnew.wxPoint(x, y)
664 | point = pcbnew.wxPoint(x, y)
665 | end = pcbnew.VECTOR2I(point.x, point.y)
666 | self.add_track(net, pos, end)
667 | self.add_via(net, end)
668 |
669 | def add_track(self, net, start, end):
670 | track = pcbnew.PCB_TRACK(self.board)
671 | track.SetStart(start)
672 | track.SetEnd(end)
673 | track.SetWidth(self.track)
674 | track.SetLayer(pcbnew.F_Cu)
675 | track.SetNetCode(net)
676 | self.board.Add(track)
677 | self.tracks.append(track)
678 |
679 | self.group.SetName("FANOUT_TRACKS_and_VIA")
680 | self.group.AddItem(track)
681 | self.board.Add(self.group)
682 | # self.groups.append(self.group)
683 |
684 |
685 | def add_via(self, net, pos):
686 | via = pcbnew.PCB_VIA(self.board)
687 | via.SetViaType(pcbnew.VIATYPE_THROUGH)
688 | if self.get_major_version() >= 7:
689 | # KiCad v7
690 | via.SetPosition(pcbnew.VECTOR2I(pos))
691 | else:
692 | # KiCad v6
693 | via.SetPosition(pos)
694 | via.SetWidth(int(self.via.m_Diameter))
695 | via.SetDrill(self.via.m_Drill)
696 | via.SetNetCode(net)
697 | self.board.Add(via)
698 | self.tracks.append(via)
699 |
700 | self.group.AddItem(via)
701 | self.board.Add(self.group)
702 |
703 | def remove_track_via(self):
704 | for item in self.tracks:
705 | self.board.Remove(item)
706 | self.tracks.clear()
707 | pcbnew.Refresh()
--------------------------------------------------------------------------------
/onekiwi/model/model.py:
--------------------------------------------------------------------------------
1 | import pcbnew
2 | from ..kicad.board import get_current_unit
3 | from .bga import BGA
4 |
5 | class Model:
6 | def __init__(self, board, logger):
7 | self.logger = logger
8 | self.unit = get_current_unit()
9 | self.board = board
10 | self.references = []
11 | self.reference = None
12 | self.skip = 0
13 | self.track = None
14 | self.via = None
15 | self.unused_pads=None
16 | self.package = None
17 | self.alignment = None
18 | self.direction = None
19 | self.indexSelected = None
20 | self.update_reference()
21 |
22 |
23 | def update_reference(self):
24 | footprints = self.board.GetFootprints()
25 | for index,footprint in enumerate(footprints):
26 | if footprint.IsSelected():
27 | self.indexSelected = index
28 | ref = str(footprint.GetReference())
29 | self.references.append(ref)
30 |
31 | def update_data(self, reference,skip, track, via, unused_pads):
32 | self.reference = reference
33 | self.skip = skip
34 | self.track = track
35 | self.via = via
36 | self.unused_pads = unused_pads
37 |
38 | def update_package(self, package, alignment, direction):
39 | self.package = package
40 | self.alignment = alignment
41 | self.direction = direction
42 |
43 | def fanout(self):
44 | if self.package == 'BGA':
45 | self.bga = BGA(
46 | self.board, self.reference,self.skip, self.track, self.via, self.unused_pads,
47 | self.alignment, self.direction, self.logger
48 | )
49 | self.bga.fanout()
50 |
51 | def remove_track_via(self):
52 | self.bga.remove_track_via()
53 |
54 |
55 |
--------------------------------------------------------------------------------
/onekiwi/plugin.py:
--------------------------------------------------------------------------------
1 | import pcbnew
2 | import os
3 | from .controller.controller import Controller
4 |
5 | class FanoutAction(pcbnew.ActionPlugin):
6 | def defaults(self):
7 | self.name = "Fanout Tools"
8 | self.category = "Modify PCB"
9 | self.description = "BGA fanout routing"
10 | self.show_toolbar_button = True # Optional, defaults to False
11 | self.icon_file_name = os.path.join(os.path.dirname(__file__), 'icon.png') # Optional
12 |
13 | def Run(self):
14 | # The entry function of the plugin that is executed on user action
15 | board = pcbnew.GetBoard()
16 | controller = Controller(board)
17 | controller.Show()
18 | pcbnew.UpdateUserInterface()
19 | FanoutAction().register() # Instantiate and register to Pcbnew
20 |
21 |
--------------------------------------------------------------------------------
/onekiwi/version.py:
--------------------------------------------------------------------------------
1 | # Update this when new version is tagged.
2 | import os
3 | import subprocess
4 |
5 | LAST_TAG = '1.1.3'
6 |
7 | def _get_git_version():
8 | plugin_path = os.path.realpath(os.path.dirname(__file__))
9 | try:
10 | git_version = subprocess.check_output(
11 | ['git', 'describe', '--tags', '--abbrev=4', '--dirty=-*'],
12 | cwd=plugin_path)
13 | if isinstance(git_version, bytes):
14 | return git_version.decode('utf-8').rstrip()
15 | else:
16 | return git_version.rstrip()
17 | except subprocess.CalledProcessError as e:
18 | print('Git version check failed: ' + str(e))
19 | except Exception as e:
20 | print('Git process cannot be launched: ' + str(e))
21 | return None
22 |
23 | version = _get_git_version() or LAST_TAG
--------------------------------------------------------------------------------
/onekiwi/view/dialog.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | ###########################################################################
4 | ## Python code generated with wxFormBuilder (version 3.10.1-0-g8feb16b)
5 | ## http://www.wxformbuilder.org/
6 | ##
7 | ## PLEASE DO *NOT* EDIT THIS FILE!
8 | ###########################################################################
9 |
10 | import wx
11 | import wx.xrc
12 |
13 | ###########################################################################
14 | ## Class FanoutDialog
15 | ###########################################################################
16 |
17 | class FanoutDialog ( wx.Dialog ):
18 |
19 | def __init__( self, parent ):
20 | wx.Dialog.__init__ ( self, parent, id = wx.ID_ANY, title = u"Fanout Tools", pos = wx.DefaultPosition, size = wx.DefaultSize, style = wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER )
21 |
22 | self.SetSizeHints( wx.DefaultSize, wx.DefaultSize )
23 |
24 | bSizer1 = wx.BoxSizer( wx.VERTICAL )
25 |
26 | bSizer5 = wx.BoxSizer( wx.VERTICAL )
27 |
28 | bSizer8 = wx.BoxSizer( wx.HORIZONTAL )
29 |
30 | sbSizer5 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Create fanouts" ), wx.VERTICAL )
31 |
32 | fgSizer1 = wx.FlexGridSizer( 0, 2, 0, 0 )
33 | fgSizer1.SetFlexibleDirection( wx.VERTICAL )
34 | fgSizer1.SetNonFlexibleGrowMode( wx.FLEX_GROWMODE_SPECIFIED )
35 |
36 | self.textSkip = wx.StaticText( sbSizer5.GetStaticBox(), wx.ID_ANY, u"Skip:", wx.DefaultPosition, wx.DefaultSize, 0 )
37 | self.textSkip.Wrap( -1 )
38 | fgSizer1.Add( self.textSkip, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
39 | self.skip = wx.SpinCtrlDouble( sbSizer5.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, wx.SP_ARROW_KEYS, 0, 10000, 2, 1 )
40 | self.skip.SetDigits( 0 )
41 | fgSizer1.Add( self.skip, 0, wx.ALL|wx.EXPAND, 5 )
42 |
43 |
44 | self.textFilttter = wx.StaticText( sbSizer5.GetStaticBox(), wx.ID_ANY, u"Filtter:", wx.DefaultPosition, wx.DefaultSize, 0 )
45 | self.textFilttter.Wrap( -1 )
46 |
47 | fgSizer1.Add( self.textFilttter, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5 )
48 |
49 | self.editFiltter = wx.TextCtrl( sbSizer5.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
50 | fgSizer1.Add( self.editFiltter, 0, wx.ALL|wx.EXPAND, 5 )
51 |
52 | self.textReference = wx.StaticText( sbSizer5.GetStaticBox(), wx.ID_ANY, u"Reference:", wx.DefaultPosition, wx.DefaultSize, 0 )
53 | self.textReference.Wrap( -1 )
54 |
55 | fgSizer1.Add( self.textReference, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
56 |
57 | choiceReferenceChoices = []
58 | self.choiceReference = wx.Choice( sbSizer5.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, choiceReferenceChoices, 0 )
59 | self.choiceReference.SetSelection( 0 )
60 | fgSizer1.Add( self.choiceReference, 1, wx.ALL|wx.EXPAND, 5 )
61 |
62 | self.textTrack = wx.StaticText( sbSizer5.GetStaticBox(), wx.ID_ANY, u"Track width:", wx.DefaultPosition, wx.DefaultSize, 0 )
63 | self.textTrack.Wrap( -1 )
64 |
65 | fgSizer1.Add( self.textTrack, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
66 |
67 | choiceTrackChoices = []
68 | self.choiceTrack = wx.Choice( sbSizer5.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, choiceTrackChoices, 0 )
69 | self.choiceTrack.SetSelection( 0 )
70 | fgSizer1.Add( self.choiceTrack, 1, wx.ALL|wx.EXPAND, 5 )
71 |
72 | self.textVia = wx.StaticText( sbSizer5.GetStaticBox(), wx.ID_ANY, u"Via size:", wx.DefaultPosition, wx.DefaultSize, 0 )
73 | self.textVia.Wrap( -1 )
74 |
75 | fgSizer1.Add( self.textVia, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
76 |
77 | choiceViaChoices = []
78 | self.choiceVia = wx.Choice( sbSizer5.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, choiceViaChoices, 0 )
79 | self.choiceVia.SetSelection( 0 )
80 | fgSizer1.Add( self.choiceVia, 1, wx.ALL|wx.EXPAND, 5 )
81 |
82 |
83 | sbSizer5.Add( fgSizer1, 1, wx.EXPAND, 5 )
84 |
85 | self.checkUnusepad = wx.CheckBox( sbSizer5.GetStaticBox(), wx.ID_ANY, u"Unused pads", wx.DefaultPosition, wx.DefaultSize, 0 )
86 | sbSizer5.Add( self.checkUnusepad, 0, wx.ALL, 5 )
87 |
88 |
89 | bSizer8.Add( sbSizer5, 3, wx.BOTTOM|wx.EXPAND|wx.RIGHT, 5 )
90 |
91 | sbSizer6 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Preview" ), wx.VERTICAL )
92 |
93 | self.bitmapPreview = wx.StaticBitmap( sbSizer6.GetStaticBox(), wx.ID_ANY, wx.NullBitmap, wx.DefaultPosition, wx.DefaultSize, 0 )
94 | sbSizer6.Add( self.bitmapPreview, 0, wx.ALL|wx.EXPAND, 5 )
95 |
96 |
97 | bSizer8.Add( sbSizer6, 2, wx.BOTTOM|wx.EXPAND|wx.LEFT, 5 )
98 |
99 |
100 | bSizer5.Add( bSizer8, 1, wx.EXPAND, 5 )
101 |
102 | sbSizer7 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Fanout length" ), wx.HORIZONTAL )
103 |
104 | self.checkUnlimited = wx.CheckBox( sbSizer7.GetStaticBox(), wx.ID_ANY, u"Unlimited", wx.DefaultPosition, wx.DefaultSize, 0 )
105 | sbSizer7.Add( self.checkUnlimited, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
106 |
107 | self.textMaximum = wx.StaticText( sbSizer7.GetStaticBox(), wx.ID_ANY, u"Maximum:", wx.DefaultPosition, wx.DefaultSize, 0 )
108 | self.textMaximum.Wrap( -1 )
109 |
110 | sbSizer7.Add( self.textMaximum, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
111 |
112 | self.editLength = wx.TextCtrl( sbSizer7.GetStaticBox(), wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.DefaultSize, 0 )
113 | sbSizer7.Add( self.editLength, 0, wx.ALL, 5 )
114 |
115 | self.textUnit = wx.StaticText( sbSizer7.GetStaticBox(), wx.ID_ANY, u"unit", wx.DefaultPosition, wx.DefaultSize, 0 )
116 | self.textUnit.Wrap( -1 )
117 |
118 | sbSizer7.Add( self.textUnit, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
119 |
120 |
121 | bSizer5.Add( sbSizer7, 0, wx.EXPAND, 5 )
122 |
123 | sbSizer8 = wx.StaticBoxSizer( wx.StaticBox( self, wx.ID_ANY, u"Placement of via fanout for:" ), wx.VERTICAL )
124 |
125 | bSizer9 = wx.BoxSizer( wx.HORIZONTAL )
126 |
127 | self.textPackage = wx.StaticText( sbSizer8.GetStaticBox(), wx.ID_ANY, u"Package:", wx.DefaultPosition, wx.DefaultSize, 0 )
128 | self.textPackage.Wrap( -1 )
129 |
130 | bSizer9.Add( self.textPackage, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
131 |
132 | choicePackageChoices = []
133 | self.choicePackage = wx.Choice( sbSizer8.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, choicePackageChoices, 0 )
134 | self.choicePackage.SetSelection( 0 )
135 | bSizer9.Add( self.choicePackage, 0, wx.ALL, 5 )
136 |
137 | self.checkSpecial = wx.CheckBox( sbSizer8.GetStaticBox(), wx.ID_ANY, u"Special package:", wx.DefaultPosition, wx.DefaultSize, 0 )
138 | bSizer9.Add( self.checkSpecial, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
139 |
140 | choiceSpecialChoices = []
141 | self.choiceSpecial = wx.Choice( sbSizer8.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, choiceSpecialChoices, 0 )
142 | self.choiceSpecial.SetSelection( 0 )
143 | bSizer9.Add( self.choiceSpecial, 0, wx.ALL, 5 )
144 |
145 |
146 | sbSizer8.Add( bSizer9, 0, wx.EXPAND, 5 )
147 |
148 | bSizer10 = wx.BoxSizer( wx.HORIZONTAL )
149 |
150 | sizerAlignment = wx.BoxSizer( wx.VERTICAL )
151 |
152 | self.textAlignment = wx.StaticText( sbSizer8.GetStaticBox(), wx.ID_ANY, u"Alignment:", wx.DefaultPosition, wx.DefaultSize, 0 )
153 | self.textAlignment.Wrap( -1 )
154 |
155 | sizerAlignment.Add( self.textAlignment, 0, wx.ALL, 5 )
156 |
157 | choiceAlignmentChoices = []
158 | self.choiceAlignment = wx.Choice( sbSizer8.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, choiceAlignmentChoices, 0 )
159 | self.choiceAlignment.SetSelection( 0 )
160 | sizerAlignment.Add( self.choiceAlignment, 0, wx.ALL|wx.EXPAND, 5 )
161 |
162 |
163 | bSizer10.Add( sizerAlignment, 1, wx.EXPAND, 5 )
164 |
165 | sizerDirection = wx.BoxSizer( wx.VERTICAL )
166 |
167 | self.textDirection = wx.StaticText( sbSizer8.GetStaticBox(), wx.ID_ANY, u"Direction:", wx.DefaultPosition, wx.DefaultSize, 0 )
168 | self.textDirection.Wrap( -1 )
169 |
170 | sizerDirection.Add( self.textDirection, 0, wx.ALL, 5 )
171 |
172 | choiceDirectionChoices = []
173 | self.choiceDirection = wx.Choice( sbSizer8.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, choiceDirectionChoices, 0 )
174 | self.choiceDirection.SetSelection( 0 )
175 | sizerDirection.Add( self.choiceDirection, 0, wx.ALL|wx.EXPAND, 5 )
176 |
177 |
178 | bSizer10.Add( sizerDirection, 1, wx.EXPAND, 5 )
179 |
180 | sizerSpace = wx.BoxSizer( wx.VERTICAL )
181 |
182 | self.textSpace = wx.StaticText( sbSizer8.GetStaticBox(), wx.ID_ANY, u"Spacing:", wx.DefaultPosition, wx.DefaultSize, 0 )
183 | self.textSpace.Wrap( -1 )
184 |
185 | sizerSpace.Add( self.textSpace, 0, wx.ALL, 5 )
186 |
187 | choiceSpaceChoices = []
188 | self.choiceSpace = wx.Choice( sbSizer8.GetStaticBox(), wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, choiceSpaceChoices, 0 )
189 | self.choiceSpace.SetSelection( 0 )
190 | sizerSpace.Add( self.choiceSpace, 0, wx.ALL|wx.EXPAND, 5 )
191 |
192 |
193 | bSizer10.Add( sizerSpace, 1, wx.EXPAND, 5 )
194 |
195 |
196 | sbSizer8.Add( bSizer10, 1, wx.EXPAND, 5 )
197 |
198 |
199 | bSizer5.Add( sbSizer8, 0, wx.EXPAND|wx.TOP, 5 )
200 |
201 |
202 | bSizer1.Add( bSizer5, 1, wx.ALL|wx.EXPAND, 5 )
203 |
204 | bSizer7 = wx.BoxSizer( wx.HORIZONTAL )
205 |
206 | self.buttonFanout = wx.Button( self, wx.ID_ANY, u"Fanout", wx.DefaultPosition, wx.DefaultSize, 0 )
207 | bSizer7.Add( self.buttonFanout, 1, wx.ALL|wx.EXPAND, 5 )
208 |
209 | self.buttonUndo = wx.Button( self, wx.ID_ANY, u"Undo", wx.DefaultPosition, wx.DefaultSize, 0 )
210 | bSizer7.Add( self.buttonUndo, 1, wx.ALL|wx.EXPAND, 5 )
211 |
212 | self.buttonClear = wx.Button( self, wx.ID_ANY, u"Clear log", wx.DefaultPosition, wx.DefaultSize, 0 )
213 | bSizer7.Add( self.buttonClear, 1, wx.ALL|wx.EXPAND, 5 )
214 |
215 | self.buttonClose = wx.Button( self, wx.ID_ANY, u"Close", wx.DefaultPosition, wx.DefaultSize, 0 )
216 | bSizer7.Add( self.buttonClose, 1, wx.ALL|wx.EXPAND, 5 )
217 |
218 |
219 | bSizer1.Add( bSizer7, 0, wx.EXPAND, 5 )
220 |
221 | self.staticLine = wx.StaticLine( self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL )
222 | bSizer1.Add( self.staticLine, 0, wx.EXPAND |wx.ALL, 5 )
223 |
224 | self.textLog = wx.TextCtrl( self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, wx.Size( -1,100 ), wx.HSCROLL|wx.TE_MULTILINE|wx.TE_READONLY )
225 | bSizer1.Add( self.textLog, 0, wx.ALL|wx.EXPAND, 5 )
226 |
227 |
228 | self.SetSizer( bSizer1 )
229 | self.Layout()
230 | bSizer1.Fit( self )
231 |
232 | self.Centre( wx.BOTH )
233 |
234 | def __del__( self ):
235 | pass
236 |
237 |
238 |
--------------------------------------------------------------------------------
/onekiwi/view/view.py:
--------------------------------------------------------------------------------
1 | import wx
2 | from .dialog import *
3 | from ..version import version
4 | import os
5 | from ..kicad.board import get_image_path
6 |
7 | class FanoutView(FanoutDialog):
8 | def __init__(self):
9 | FanoutDialog.__init__(self, None)
10 | self.SetTitle('Fanout Tool v%s' % version)
11 | def GetSkipIndex(self):
12 | return int(self.skip.GetValue())
13 |
14 | def AddReferences(self, references):
15 | self.choiceReference.Append(references)
16 |
17 | def SetIndexReferences(self, index):
18 | self.choiceReference.SetSelection(index)
19 |
20 | def ClearReferences(self):
21 | self.choiceReference.Clear()
22 |
23 | def GetReferenceSelected(self):
24 | index = self.choiceReference.GetSelection()
25 | value = self.choiceReference.GetString(index)
26 | return value
27 | def AddTracksWidth(self, tracks):
28 | self.choiceTrack.Append(tracks)
29 | self.choiceTrack.SetSelection(0)
30 |
31 | def AddViasSize(self, vias):
32 | self.choiceVia.Append(vias)
33 | self.choiceVia.SetSelection(0)
34 | def GetCheckUnusepad(self):
35 | return self.checkUnusepad.GetValue()
36 |
37 | def GetTrackSelectedIndex(self):
38 | return self.choiceTrack.GetSelection()
39 |
40 | def GetViaSelectedIndex(self):
41 | return self.choiceVia.GetSelection()
42 |
43 | def AddPackageType(self, items, index):
44 | self.choicePackage.Append(items)
45 | self.choicePackage.SetSelection(index)
46 |
47 | def GetPackageIndex(self):
48 | index = self.choicePackage.GetSelection()
49 | return index
50 |
51 | def GetPackageValue(self):
52 | index = self.choicePackage.GetSelection()
53 | value = self.choicePackage.GetString(index)
54 | return value
55 |
56 | def AddAlignment(self, items):
57 | self.choiceAlignment.Append(items)
58 | self.choiceAlignment.SetSelection(0)
59 |
60 | def ClearAlignment(self):
61 | self.choiceAlignment.Clear()
62 |
63 | def GetAlignmentIndex(self):
64 | index = self.choiceAlignment.GetSelection()
65 | return index
66 |
67 | def GetAlignmentValue(self):
68 | index = self.choiceAlignment.GetSelection()
69 | value = self.choiceAlignment.GetString(index)
70 | return value
71 |
72 | def AddDirection(self, items):
73 | self.choiceDirection.Append(items)
74 | self.choiceDirection.SetSelection(0)
75 |
76 | def ClearDirection(self):
77 | self.choiceDirection.Clear()
78 |
79 | def GetDirectionIndex(self):
80 | index = self.choiceDirection.GetSelection()
81 | return index
82 |
83 | def GetDirectionValue(self):
84 | index = self.choiceDirection.GetSelection()
85 | print(index)
86 | value = self.choiceDirection.GetString(index)
87 | return value
88 |
89 | def SetImagePreview(self, name):
90 | path = get_image_path()
91 | image = os.path.join(path, name)
92 | self.bitmapPreview.SetBitmap(wx.Bitmap(image))
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -e
2 |
3 | version=$(git describe --tags --dirty)
4 | name=$(echo fanout-tool-$version.zip)
5 |
6 | echo "Building release $version"
7 | cp metadata.json.template metadata.json
8 | sed -i -e "s/VERSION/$version/g" metadata.json
9 | sed -i '/download_/d' metadata.json
10 | sed -i '/install_size/d' metadata.json
11 |
12 | mkdir resources
13 | cp icon/icon_64x64.png resources/
14 | mv resources/icon_64x64.png resources/icon.png
15 |
16 | mkdir plugins
17 | cp __init__.py plugins/
18 | cp -r onekiwi/ plugins/
19 |
20 | zip -r $name plugins resources metadata.json
21 |
22 | rm -rf plugins
23 | rm -rf resources
24 |
25 | sha=$(sha256sum $name | cut -d' ' -f1)
26 | size=$(du -b $name | cut -f1)
27 | installSize=$(unzip -l $name | tail -1 | xargs | cut -d' ' -f1)
28 |
29 | cp metadata.json.template metadata.json
30 | sed -i -e "s/VERSION/$version/g" metadata.json
31 | sed -i -e "s/SHA256/$sha/g" metadata.json
32 | sed -i -e "s/DOWNLOAD_SIZE/$size/g" metadata.json
33 | sed -i -e "s/INSTALL_SIZE/$installSize/g" metadata.json
34 |
35 | ls -lh $name metadata.json
36 |
37 |
--------------------------------------------------------------------------------