-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathpcbPanelize
executable file
·333 lines (310 loc) · 10.8 KB
/
pcbPanelize
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
#!/usr/bin/env python2
# -*- encoding: utf-8 -*-
# -*- coding: utf-8 -*-
#All greetings may go to LordBlick on gmail… ;)
from pcbnew import LoadBoard, DRAWSEGMENT as DS, DIMENSION as DIMN, \
Edge_Cuts as Cutout, wxPoint as wxPt, FromMils, GetKicadConfigPath as cfgPath
from os import path as ph
class panelizePCB:
def __init__(self):
self.uiInit()
self.appStart()
def uiInit(self):
from uiPanelize import panelizeUI
ui = self.ui = panelizeUI()
gtk = ui.gtk
gdk = gtk.gdk
ui.mainWindow.connect("destroy", lambda xargs: self.Exit())
self.TARGET_TYPE_URI_LIST = 80
self.lsDragAndDrop = [("text/uri-list", 0, self.TARGET_TYPE_URI_LIST )]
ui.logView.connect("drag-data-received", self.dragReceived)
ui.logView.drag_dest_set(gtk.DEST_DEFAULT_MOTION|gtk.DEST_DEFAULT_HIGHLIGHT|gtk.DEST_DEFAULT_DROP,
self.lsDragAndDrop, gdk.ACTION_COPY)
ui.logView.connect("drag-motion", self.dragMotion)
ui.buttonFileName.connect("clicked", lambda xargs: self.appFileName())
ui.buttonProceed.connect("clicked", lambda xargs: self.appPanelize())
ui.buttonExit.connect("clicked", lambda xargs: self.Exit())
def errReport(self, txt):
ui = self.ui
ui.logView.insert_end(("Script Version: v.%0.2f. Error report:\n"+txt) % ui.version)
def brdBounds(self, pcb):
outPoints = []
for Draw in pcb.GetDrawings():
if type(Draw) is DS and(Draw.GetShapeStr()=='Line')\
and(Draw.GetLayer()==Cutout):
Draw.SetWidth(FromMils(10))
for pnt in (Draw.GetStart(), Draw.GetEnd()):
if pnt not in outPoints:
outPoints.append(pnt)
x_min, y_min = x_max, y_max = outPoints[0]
for x, y in outPoints:
x_min = min(x_min, x)
y_min = min(y_min, y)
x_max = max(x_max, x)
y_max = max(y_max, y)
return (x_min, y_min), (x_max, y_max)
def brdRotate(self, wxCenter, angle):
for Item in self.lsItems:
Item.Rotate(wxCenter, angle)
def brdPosite(self, ptFrom, ptTo):
vect = wxPt(ptTo[0]-ptFrom[0], ptTo[1]-ptFrom[1])
for Item in self.lsItems:
Item.Move(vect)
def brdItemize(self, pcb):
self.lsItems = []
nullType = type(None)
for ItemStr in ('Drawings', 'Tracks', 'Modules'):
for idx, Item in enumerate(getattr(pcb, 'Get'+ItemStr)()):
if type(Item)==nullType:
raise TypeError, "Null Object Error#%i, expected %s Type…" % (idx+1, ItemStr[:-1])
Item.remove = False
self.lsItems.append(Item)
nZones = pcb.GetAreaCount()
if nZones:
for idx in range(nZones):
Zone = pcb.GetArea(idx)
if type(Zone)==nullType:
raise TypeError, "Null Object Error#%i, expected Zone/Area Type…" % (idx+1,)
Zone.remove = False
self.lsItems.append(Zone)
def brdDrawInEdge(self, Item, orginBounds):
if type(Item) is not DS:
return False
if Item.GetLayer()!=Cutout:
return False
(xa, ya), (xb, yb) = orginBounds
if Item.GetShapeStr()=='Line':
x1, y1 = Item.GetStart()
x2, y2 = Item.GetEnd()
if x1==x2 and(x1 in (xa, xb)):
return True
if y1==y2 and(y1 in (ya, yb)):
return True
# Other Shapes in Future
return False
def brdPanel(self, pcb, nx, ny, spaceX=0, spaceY=0, margin=0, SolMsk=0, angle=0, debug=False):
if nx<1 or(ny<1):
raise ValueError, "Columns(%i) or Rows(%i) cannot be less than 1…" % (nx, ny)
if SolMsk:
for Module in pcb.GetModules():
Module.SetLocalSolderMaskMargin(SolMsk)
for Pad in Module.Pads():
Pad.SetLocalSolderMaskMargin(0)
bd = self.brdBounds(pcb)
self.brdItemize(pcb)
wxCenter = wxPt((bd[0][0]+bd[1][0])/2, (bd[0][1]+bd[1][1])/2)
if angle:
self.brdRotate( wxCenter, angle)
bs = FromMils(500), FromMils(500)
wxBase = wxPt(*bs)
bd = self.brdBounds(pcb)
self.brdPosite(bd[0], wxBase)
bd = self.brdBounds(pcb)
vx, vy = bd[1][0]-bd[0][0], bd[1][1]-bd[0][1]
for x in range(nx):
for y in range(ny):
if x==0 and(y==0):
continue # Skip orgin
vect = wxPt(x*(vx+spaceX), y*(vy+spaceY))
for Item in self.lsItems:
if Item.remove:
continue
elif self.brdDrawInEdge(Item, bd) or(type(Item) is DIMN):
Item.remove = True
continue # do not clone dimmensions or edge lines, a last one will be exchanged with generated grid
try:
newItem = Item.Duplicate()
except Exception as err:
try:
newItem = Item.Duplicate(True)
except Exception as err:
self.errReport("Temporaty unsolvable error due to different versions of kicad pcb builds.\n"+\
"\nError message:\n\t%s\n" % (err))
return
pcb.Add(newItem)
newItem.Move(vect)
dn = lambda max_n, n, vn, space:\
(0, ((vn, space)[((n+bool(margin)+bool(space))%2)*bool(space)], margin)[(n in(1, max_n))*bool(margin)])[bool(n)]
max_nx = nx+2*bool(margin)+(nx-1)*bool(spaceX)
endX = bs[0]+nx*vx+(nx-1)*spaceX+margin
max_ny = ny+2*bool(margin)+(ny-1)*bool(spaceY)
endY = bs[1]+ny*vy+(ny-1)*spaceY+margin
xp = bs[0]-margin
for nx in range(max_nx+1):
line = DS(pcb)
line.SetLayer(Cutout)
line.SetWidth(FromMils(10))
xp += dn(max_nx, nx, vx, spaceX)
line.SetStart(wxPt(xp, bs[1]-margin))
line.SetEnd(wxPt(xp, endY))
if debug:
print("|:(%i, %i)\n (%i, %i)" % (xp, bs[1]-margin, xp, endY))
pcb.Add(line)
if debug:
print('')
yp = bs[1]-margin
for ny in range(max_ny+1):
line = DS(pcb)
line.SetLayer(Cutout)
line.SetWidth(FromMils(10))
yp += dn(max_ny, ny, vy, spaceY)
line.SetStart(wxPt(bs[0]-margin, yp))
line.SetEnd(wxPt(endX, yp))
if debug:
print("-:(%i, %i)\n (%i, %i)" % (bs[0]-margin, yp, endX, yp))
pcb.Add(line)
if debug:
print('')
nItems = len(self.lsItems)
for n in range(nItems):
idx = nItems-1-n
Item = self.lsItems[idx]
if Item.remove:
self.lsItems.pop(idx)
pcb.Delete(Item)
pcb.SetAuxOrigin(wxBase)
pcb.SetGridOrigin(wxBase)
return
def appUpdateFilename(self, bUpEmptyTxt=True):
ui = self.ui
fileName = self.cfg['lastFileName'] if (hasattr(self, 'cfg')\
and(self.cfg.has_key('lastFileName'))) else ''
if bUpEmptyTxt or(fileName):
ui.txtFilename.set_text(fileName.replace(ph.expanduser('~'), '~'))
ui.buttonProceed.set_sensitive(bool(fileName))
def appDropFilename(self):
self.cfg.pop('lastFileName')
self.appUpdateFilename()
def appPanelize(self):
ui = self.ui
if self.cfg.has_key('lastFileName') and(ph.isfile(self.cfg['lastFileName'])):
fileNamePCB = self.cfg['lastFileName']
uiFileName = fileNamePCB.replace(ph.expanduser('~'), '~')
fileNameSave = "%s-panel.%s" % tuple(fileNamePCB.rsplit('.', 1))
try:
pcb = LoadBoard(fileNamePCB)
except Exception as err:
self.errReport(("Unrecognized data found. Checkout that file:\n"+\
"\t'%s'\nError message:\n\t%s\n") % (uiFileName, err))
self.appDropFilename()
return
ui.logView.insert_end("Reading file:\n\t'%s'\n"% uiFileName)
Margin = ui.Margin.get_value() if ui.Margin.get_checked() else 0
SpaceX = ui.SpaceX.get_value() if ui.SpaceX.get_checked() else 0
SpaceY = ui.SpaceY.get_value() if ui.SpaceY.get_checked() else 0
try:
self.brdPanel(pcb, int(ui.Cols.get_value()), int(ui.Rows.get_value()),
spaceX=SpaceX, spaceY=SpaceY, margin=Margin,
SolMsk=FromMils(4), angle=ui.rfAngle.get_value())
except Exception as err:
self.errReport("Something went wrong. Contact developers.\n"+\
"Provide info about current configuration and processed file:\n"+\
"\t'%s'\nError message:\n\t%s\n" % (uiFileName, err))
self.appDropFilename()
return
pcb.Save(fileNameSave)
ui.logView.insert_end("Saved in file:\n\t'%s'\n"% fileNameSave.replace(ph.expanduser('~'), '~'))
def appFileName(self):
ui = self.ui
apw = ui.apw
lastFileName = self.cfg.get('lastFileName')
lastDir = ph.dirname(lastFileName) if lastFileName else ui.callDir
newFilename = apw.dialogChooseFile(parent=ui.mainWindow,
startDir=lastDir, title="Select file to read", bShowHidden=True)
if newFilename:
self.cfg['lastFileName'] = newFilename
self.appUpdateFilename()
def uriPath(self, uri):
from urllib import url2pathname as u2p
"get the path to file"
path = ""
# Windows nautilus, rox xffm
for prefix in ('file:\\\\\\', 'file://', 'file:'):
if uri.startswith(prefix):
path = u2p(uri[len(prefix):]).strip('\r\n\x00')
break
return path
def dragReceived(self, widget, context, x, y, selection, target_type, timestamp):
ui = self.ui
if target_type == self.TARGET_TYPE_URI_LIST:
uri = selection.data.strip('\r\n\x00')
self.cfg['lastFileName'] = self.uriPath(uri.split(None, 1)[0])
self.appUpdateFilename()
return True
def dragMotion(self, widget, context, x, y, timestamp):
ui = self.ui
if not(ui.mainWindow.is_active()):
ui.mainWindow.present()
gdk = ui.gtk.gdk
context.drag_status(gdk.ACTION_COPY, timestamp)
return True
def appCfgLoad(self):
ui = self.ui
self.cfg = {}
self.cfgFileName = ph.normpath("%s/%s.conf" % (cfgPath(), ph.basename(ph.abspath(__file__))))
print("cfg:%s" % self.cfgFileName.replace(ph.expanduser('~'), '~'))
if ph.isfile(self.cfgFileName):
hFileCfg = open(self.cfgFileName, 'r')
cfgData = hFileCfg.read()
hFileCfg.close()
for inputLine in cfgData.splitlines():
inputLine = inputLine.strip()
if inputLine and ':' in inputLine:
name, value = inputLine.split(':', 1)
if name=='lastFileName':
value = value.strip()
self.cfg[name] = value
self.lastcfg = self.cfg.copy()
else:
self.lastcfg = None
if self.cfg.has_key('Angle'):
if self.cfg['Angle'].isdigit():
cbSet = int(self.cfg['Angle'])
if cbSet in range(4):
ui.rfAngle.set_active(cbSet)
else:
self.cfg.pop('Angle')
for keyD in('Cols', 'Rows', 'Margin', 'SpaceX', 'SpaceY'):
if self.cfg.has_key(keyD):
try:
value = int(self.cfg[keyD])
getattr(ui, keyD).set_value(value)
except ValueError:
self.cfg.pop(keyD)
for keyD in('CheckMargin', 'CheckSpaceX', 'CheckSpaceY'):
if self.cfg.has_key(keyD):
try:
value = bool(int(self.cfg[keyD]))
getattr(ui, keyD[5:]).set_checked(value)
except ValueError:
self.cfg.pop(keyD)
def appCfgStore(self):
ui = self.ui
for keyD in('Cols', 'Rows', 'Margin', 'SpaceX', 'SpaceY'):
self.cfg[keyD] = "%i" % getattr(ui, keyD).get_value()
for keyD in('CheckMargin', 'CheckSpaceX', 'CheckSpaceY'):
self.cfg[keyD] = "%i" % getattr(ui, keyD[5:]).get_checked()
self.cfg['Angle'] = "%i" % ui.rfAngle.get_active()
cfgDir = ph.dirname(self.cfgFileName)
from os import makedirs
if not(ph.isdir(cfgDir)):
makedirs(cfgDir, 0o755)
if self.lastcfg!=self.cfg:
hFileCfg = open(self.cfgFileName, 'w')
for cfg in self.cfg.keys():
hFileCfg.write(" % s:%s\n" % (cfg, self.cfg[cfg]))
hFileCfg.close()
print("Written config:%s...\n" % self.cfgFileName)
def appStart(self):
self.appCfgLoad()
self.appUpdateFilename(bUpEmptyTxt=False)
self.ui.uiEnter()
def appStop(self):
self.appCfgStore()
def Exit(self):
print("Exiting...\n")
self.appStop()
self.ui.uiExit()
# Entry point
if __name__ == "__main__":
panelizePCB()