Skip to content

Commit 86cff67

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents 209ea18 + f972993 commit 86cff67

File tree

7 files changed

+144
-39
lines changed

7 files changed

+144
-39
lines changed

.github/workflows/test-kikit.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ jobs:
4141
- name: Build PCM package
4242
run: make pcm
4343
- name: Upload kikit-pcm package artifact
44-
uses: actions/upload-artifact@v3
44+
uses: actions/upload-artifact@v4
4545
with:
4646
name: kikit-pcm
4747
path: build/pcm-kikit
4848
retention-days: 7
4949
- name: Upload kikit-lib-pcm package artifact
50-
uses: actions/upload-artifact@v3
50+
uses: actions/upload-artifact@v4
5151
with:
5252
name: kikit-lib-pcm
5353
path: build/pcm-kikit-lib

kikit/actionPlugins/panelize.py

Lines changed: 89 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import time
2+
import traceback
13
from kikit.defs import EDA_TEXT_HJUSTIFY_T, EDA_TEXT_VJUSTIFY_T
24
from pcbnewTransition import pcbnew, isV8
35
from kikit.panelize_ui_impl import loadPresetChain, obtainPreset, mergePresets
@@ -23,6 +25,7 @@ def run(self):
2325
super().run()
2426
except Exception as e:
2527
self.exception = e
28+
self.traceback = traceback.format_exc()
2629

2730
def replaceExt(file, ext):
2831
return os.path.splitext(file)[0] + ext
@@ -46,41 +49,61 @@ def presetDifferential(source, target):
4649
return result
4750

4851

49-
def transplateBoard(source, target):
52+
def transplateBoard(source, target, update=lambda x: None):
53+
CLEAR_MSG = "Clearing the old board in UI"
54+
RENDER_MSG = "Rendering the new board in UI"
5055
items = chain(
5156
list(target.GetDrawings()),
5257
list(target.GetFootprints()),
5358
list(target.GetTracks()),
5459
list(target.Zones()))
5560
for x in items:
61+
update(CLEAR_MSG)
5662
target.Remove(x)
5763

5864
for x in list(target.GetNetInfo().NetsByNetcode().values()):
65+
update(CLEAR_MSG)
5966
target.Remove(x)
6067

68+
update(RENDER_MSG)
69+
target.SetProperties(source.GetProperties())
70+
update(RENDER_MSG)
71+
target.SetPageSettings(source.GetPageSettings())
72+
update(RENDER_MSG)
73+
target.SetTitleBlock(source.GetTitleBlock())
74+
if not isV8():
75+
update(RENDER_MSG)
76+
target.SetZoneSettings(source.GetZoneSettings())
77+
6178
for x in source.GetDrawings():
79+
update(RENDER_MSG)
6280
appendItem(target, x)
6381
for x in source.GetFootprints():
82+
update(RENDER_MSG)
6483
appendItem(target, x)
6584
for x in source.GetTracks():
85+
update(RENDER_MSG)
6686
appendItem(target, x)
6787
for x in source.Zones():
88+
update(RENDER_MSG)
6889
appendItem(target, x)
6990

7091
for n in [n for _, n in source.GetNetInfo().NetsByNetcode().items()]:
92+
update(RENDER_MSG)
7193
target.Add(n)
7294

95+
update(RENDER_MSG)
7396
d = target.GetDesignSettings()
7497
d.CloneFrom(source.GetDesignSettings())
7598

76-
target.SetProperties(source.GetProperties())
77-
target.SetPageSettings(source.GetPageSettings())
78-
target.SetTitleBlock(source.GetTitleBlock())
79-
if not isV8():
80-
target.SetZoneSettings(source.GetZoneSettings())
99+
81100

82101
def drawTemporaryNotification(board, sourceFilename):
83-
bbox = findBoardBoundingBox(board)
102+
try:
103+
bbox = findBoardBoundingBox(board)
104+
except Exception:
105+
# If the output is empty...
106+
bbox = pcbnew.BOX2I(pcbnew.VECTOR2I(0, 0), pcbnew.VECTOR2I(0, 0))
84107

85108
text = pcbnew.PCB_TEXT(board)
86109
text.SetLayer(pcbnew.Margin)
@@ -278,6 +301,8 @@ def __init__(self, parent=None, board=None, preset=None):
278301

279302
self.board = board
280303
self.dirty = False
304+
self.progressDlg = None
305+
self.lastPulse = time.time()
281306

282307
topMostBoxSizer = wx.BoxSizer(wx.VERTICAL)
283308

@@ -424,14 +449,49 @@ def OnResize(self):
424449
def OnClose(self, event):
425450
self.EndModal(0)
426451

452+
def _updatePanelizationProgress(self, message, force=False):
453+
self.phase = message
454+
now = time.time()
455+
456+
if now - self.lastPulse > 1 / 50 or force:
457+
self.lastPulse = now
458+
if self.progressDlg is not None:
459+
self.progressDlg.Pulse(newmsg=f"Running KiKit: {self.phase}")
460+
if force:
461+
self.progressDlg.Refresh()
462+
wx.GetApp().Yield()
463+
464+
def _panelizationRoutine(self, tempdir, input, panelFile, preset):
465+
panelize_ui.doPanelization(input, panelFile, preset)
466+
467+
# KiCAD 6 does something strange here, so we will load an empty
468+
# file if we read it directly, but we can always make a copy and
469+
# read that. Copying a file can be lengthy, so we will copy the
470+
# file in a thread.
471+
copyPanelName = os.path.join(tempdir, "panel-copy.kicad_pcb")
472+
shutil.copy(panelFile, copyPanelName)
473+
try:
474+
shutil.copy(replaceExt(panelFile, ".kicad_pro"), replaceExt(copyPanelName, "kicad_pro"))
475+
shutil.copy(replaceExt(panelFile, ".kicad_prl"), replaceExt(copyPanelName, "kicad_prl"))
476+
except FileNotFoundError:
477+
# We don't care if we didn't manage to copy the files
478+
pass
479+
self.temporary_panel = pcbnew.LoadBoard(copyPanelName)
480+
481+
def _pulseWhilePcbnewRefresh(self):
482+
while not self.refreshDone:
483+
time.sleep(1/50)
484+
self._updatePanelizationProgress("Pcbnew is updating the preview")
485+
486+
427487
def OnPanelize(self, event):
428488
with tempfile.TemporaryDirectory(prefix="kikit") as dirname:
429489
try:
430-
progressDlg = wx.ProgressDialog(
431-
"Running kikit", "Running kikit, please wait",
490+
self.progressDlg = wx.ProgressDialog(
491+
"Running kikit", f"Running KiKit:",
432492
parent=self)
433-
progressDlg.Show()
434-
progressDlg.Pulse()
493+
self._updatePanelizationProgress("Starting up")
494+
self.progressDlg.Show()
435495

436496
args = self.kikitArgs()
437497
preset = obtainPreset([], **args)
@@ -458,43 +518,38 @@ def OnPanelize(self, event):
458518
dlg.ShowModal()
459519
dlg.Destroy()
460520
return
461-
thread = ExceptionThread(target=panelize_ui.doPanelization,
462-
args=(input, panelFile, preset))
521+
522+
# We run as much as possible in a separate thread to not stall
523+
# the UI...
524+
thread = ExceptionThread(target=self._panelizationRoutine,
525+
args=(dirname, input, panelFile, preset))
463526
thread.daemon = True
464527
thread.start()
465528
while True:
466-
progressDlg.Pulse()
467-
thread.join(timeout=1)
529+
self._updatePanelizationProgress("Panelization")
530+
thread.join(timeout=1 / 50)
468531
if not thread.is_alive():
469532
break
470-
if thread.exception and not isinstance(thread.exception, NonFatalErrors):
533+
if thread.exception:
471534
raise thread.exception
472-
# KiCAD 6 does something strange here, so we will load
473-
# an empty file if we read it directly, but we can always make
474-
# a copy and read that:
475-
copyPanelName = os.path.join(dirname, "panel-copy.kicad_pcb")
476-
shutil.copy(panelFile, copyPanelName)
477-
try:
478-
shutil.copy(replaceExt(panelFile, ".kicad_pro"), replaceExt(copyPanelName, "kicad_pro"))
479-
shutil.copy(replaceExt(panelFile, ".kicad_prl"), replaceExt(copyPanelName, "kicad_prl"))
480-
except FileNotFoundError:
481-
# We don't care if we didn't manage to copy the files
482-
pass
483-
panel = pcbnew.LoadBoard(copyPanelName)
484-
transplateBoard(panel, self.board)
535+
536+
# ...however, transplate board and pcbnew.Refresh has to happen
537+
# in the main thread
538+
transplateBoard(self.temporary_panel, self.board, self._updatePanelizationProgress)
485539
drawTemporaryNotification(self.board, panelFile)
540+
self._updatePanelizationProgress("Pcbnew will now refresh panel, the UI might freeze", force=True)
541+
pcbnew.Refresh()
542+
self._updatePanelizationProgress("Done", force=True)
486543
self.dirty = True
487-
if thread.exception:
488-
raise thread.exception
489544
except Exception as e:
490545
dlg = wx.MessageDialog(
491546
None, f"Cannot perform:\n\n{e}", "Error", wx.OK)
492547
dlg.ShowModal()
493548
dlg.Destroy()
494549
finally:
495-
progressDlg.Hide()
496-
progressDlg.Destroy()
497-
pcbnew.Refresh()
550+
self.progressDlg.Hide()
551+
self.progressDlg.Destroy()
552+
self.progressDlg = None
498553

499554
def populateInitialValue(self, initialPreset=None):
500555
preset = loadPresetChain([":default"])

kikit/common.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ def collectEdges(board, layerId, sourceArea=None):
7575
continue
7676
if isinstance(edge, pcbnew.PCB_DIMENSION_BASE):
7777
continue
78+
if isinstance(edge, pcbnew.PCB_TEXT):
79+
continue
7880
if not sourceArea or fitsIn(edge.GetBoundingBox(), sourceArea):
7981
edges.append(edge)
8082
return edges

kikit/drc.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ class DrcExclusion:
6161
objects: List[pcbnew.BOARD_ITEM] = field(default_factory=list)
6262

6363
def eqRepr(self) -> Tuple[str, Union[Tuple[str, str], str]]:
64+
if len(self.objects) == 0:
65+
return (self.type, ())
6466
if len(self.objects) == 1:
6567
objRepr = str(self.objects[0].m_Uuid.AsString()) if isinstance(self.objects[0], pcbnew.BOARD_ITEM) else self.objects[0]
6668
return (self.type, objRepr)

kikit/export.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ def pasteDxfExport(board, plotDir):
184184
popt.SetScale(1)
185185
popt.SetDXFPlotUnits(DXF_UNITS_MILLIMETERS)
186186
popt.SetDXFPlotPolygonMode(False)
187+
popt.SetDrillMarksType(DRILL_MARKS_NO_DRILL_SHAPE)
187188

188189
plot_plan = [
189190
# name, id, comment

kikit/panelize.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ def __init__(self, errors: List[Tuple[KiPoint, str]]) -> None:
5252
for pos, err in errors:
5353
message += f"- Location [{toMm(pos[0])}, {toMm(pos[1])}]\n"
5454
message += textwrap.indent(err, " ")
55+
if not message.endswith("\n"):
56+
message += "\n"
57+
message = message[:-1]
5558
super().__init__(message)
5659

5760
def identity(x):
@@ -348,7 +351,7 @@ def isBoardEdge(edge):
348351
349352
The rule is: all drawings on Edge.Cuts layer are edges.
350353
"""
351-
return isinstance(edge, pcbnew.PCB_SHAPE) and edge.GetLayerName() == "Edge.Cuts"
354+
return isinstance(edge, pcbnew.PCB_SHAPE) and edge.GetLayer() == pcbnew.Edge_Cuts
352355

353356
def tabSpacing(width, count):
354357
"""
@@ -556,6 +559,7 @@ def save(self, reconstructArcs: bool=False, refillAllZones: bool=False,
556559
for e in boardsEdges:
557560
e.SetWidth(edgeWidth)
558561

562+
self._validateVCuts()
559563
vcuts = self._renderVCutH() + self._renderVCutV()
560564
keepouts = []
561565
for cut, clearanceArea in vcuts:
@@ -615,7 +619,10 @@ def save(self, reconstructArcs: bool=False, refillAllZones: bool=False,
615619
if zName.startswith("KIKIT_zone_"):
616620
zonesToRefill.append(zone)
617621
zone.SetZoneName(originalZoneNames[zName])
618-
fillerTool.Fill(zonesToRefill)
622+
if len(zonesToRefill) > 0:
623+
# Even if there are no zones to refill, the refill algorithm takes
624+
# non-trivial time to compute, hence, skip it.
625+
fillerTool.Fill(zonesToRefill)
619626

620627
fillBoard.Save(self.filename)
621628
self._adjustPageSize()
@@ -1201,6 +1208,32 @@ def _setVCutLabelStyle(self, label, origin, position):
12011208
label.SetTextSize(toKiCADPoint((self.vCutSettings.textSize, self.vCutSettings.textSize)))
12021209
label.SetHorizJustify(EDA_TEXT_HJUSTIFY_T.GR_TEXT_HJUSTIFY_LEFT)
12031210

1211+
def _validateVCuts(self, tolerance=fromMm(1)):
1212+
"""
1213+
Validates V-cuts for cuttitng the PCBs. Renders the violations into the
1214+
PCB as a side effect.
1215+
"""
1216+
if len(self.hVCuts) == 0 and len(self.vVCuts) == 0:
1217+
return
1218+
1219+
collisionPolygons = shapely.ops.unary_union([x.substrates.buffer(-tolerance) for x in self.substrates])
1220+
minx, miny, maxx, maxy = self.panelBBox()
1221+
1222+
lines = \
1223+
[LineString([(minx, y), (maxx, y)]) for y in self.hVCuts] + \
1224+
[LineString([(x, miny), (x, maxy)]) for x in self.vVCuts]
1225+
1226+
error_message = "V-Cut cuts the original PCBs. You should:\n"
1227+
error_message += "- either reconsider your tab placement,\n"
1228+
error_message += "- or use different cut type – e.g., mouse bites."
1229+
for line in lines:
1230+
for geom in listGeometries(collisionPolygons.intersection(line)):
1231+
if geom.is_empty:
1232+
continue
1233+
annotationPos = sorted(geom.coords, key=lambda p: -p[1])[0]
1234+
self._renderLines([geom], Layer.Margin)
1235+
self.reportError(toKiCADPoint(annotationPos), error_message)
1236+
12041237
def _renderVCutV(self):
12051238
""" return list of PCB_SHAPE V-Cuts """
12061239
bBox = self.boardSubstrate.boundingBox()
@@ -1508,7 +1541,7 @@ def makeVCuts(self, cuts, boundCurves=False, offset=fromMm(0)):
15081541
message += "- your vertical or horizontal PCB edges are not precisely vertical or horizontal.\n"
15091542
message += "Modify the design or accept curve approximation via V-cuts."
15101543
self._renderLines([cut], Layer.Margin)
1511-
self.reportError(toKiCADPoint(cut[0]), message)
1544+
self.reportError(toKiCADPoint(cut.coords[0]), message)
15121545
continue
15131546
cut = cut.simplify(1).parallel_offset(offset, "left")
15141547
start = roundPoint(cut.coords[0])

kikit/substrate.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ def getStartPoint(geom):
5151
point = geom.GetStart() + pcbnew.VECTOR2I(geom.GetRadius(), 0)
5252
elif geom.GetShape() == STROKE_T.S_RECT:
5353
point = geom.GetStart()
54+
elif geom.GetShape() == STROKE_T.S_POLYGON:
55+
# Polygons don't use the properties for start point, look into the
56+
# geometry
57+
point = geom.GetPolyShape().Outline(0).CPoints()[0]
5458
else:
5559
point = geom.GetStart()
5660
return point
@@ -62,6 +66,14 @@ def getEndPoint(geom):
6266
elif geom.GetShape() == STROKE_T.S_RECT:
6367
# Rectangle is closed, so it starts at the same point as it ends
6468
point = geom.GetStart()
69+
elif geom.GetShape() == STROKE_T.S_POLYGON:
70+
# Polygons don't use the properties for start point, look into the
71+
# geometry
72+
outline = geom.GetPolyShape().Outline(0)
73+
if outline.IsClosed():
74+
point = outline.CPoints()[0]
75+
else:
76+
point = outline.CPoints()[-1]
6577
else:
6678
if hasattr(geom, 'IsClosed'):
6779
point = geom.GetStart() if geom.IsClosed() else geom.GetEnd()

0 commit comments

Comments
 (0)