Skip to content

Commit

Permalink
Introduce error annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
yaqwsx committed Mar 17, 2024
1 parent 6bff8c2 commit d1bbcdf
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 41 deletions.
6 changes: 4 additions & 2 deletions kikit/actionPlugins/panelize.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pcbnewTransition import pcbnew, isV8
from kikit.panelize_ui_impl import loadPresetChain, obtainPreset, mergePresets
from kikit import panelize_ui
from kikit.panelize import appendItem
from kikit.panelize import NonFatalErrors, appendItem
from kikit.common import PKG_BASE
from .common import initDialog, destroyDialog
import kikit.panelize_ui_sections
Expand Down Expand Up @@ -420,7 +420,7 @@ def OnPanelize(self, event):
thread.join(timeout=1)
if not thread.is_alive():
break
if thread.exception:
if thread.exception and not isinstance(thread.exception, NonFatalErrors):
raise thread.exception
# KiCAD 6 does something strange here, so we will load
# an empty file if we read it directly, but we can always make
Expand All @@ -436,6 +436,8 @@ def OnPanelize(self, event):
panel = pcbnew.LoadBoard(copyPanelName)
transplateBoard(panel, self.board)
self.dirty = True
if thread.exception:
raise thread.exception
except Exception as e:
dlg = wx.MessageDialog(
None, f"Cannot perform:\n\n{e}", "Error", wx.OK)
Expand Down
12 changes: 12 additions & 0 deletions kikit/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,18 @@ def isLinestringCyclic(line):
c = line.coords
return c[0] == c[-1] or isinstance(line, LinearRing)

def constructArrow(origin, direction, distance: float, tipSize: float) -> shapely.LineString:
origin = np.array(origin)
direction = np.array(direction)

endpoint = origin + direction * distance

tipEndpoint1 = endpoint + tipSize / 2 * (-direction - np.array([-direction[1], direction[0]]))
tipEndpoint2 = endpoint + tipSize / 2 * (-direction + np.array([-direction[1], direction[0]]))

arrow = shapely.LineString([origin, endpoint, tipEndpoint1, tipEndpoint2, endpoint])
return arrow

def fromOpt(object, default):
"""
Given an object, return it if not None. Otherwise return default
Expand Down
92 changes: 71 additions & 21 deletions kikit/panelize.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from copy import deepcopy
import itertools
import textwrap
from pcbnewTransition import pcbnew, isV6
from kikit import sexpr
from kikit.common import normalize
Expand Down Expand Up @@ -41,6 +42,17 @@ class PanelError(RuntimeError):
class TooLargeError(PanelError):
pass

class NonFatalErrors(PanelError):
def __init__(self, errors: List[Tuple[KiPoint, str]]) -> None:
multiple = len(errors) > 1

message = f"There {'are' if multiple else 'is'} {len(errors)} error{'s' if multiple else ''} in the panel. The panel with error markers was saved for inspection.\n\n"
message += "The following errors occurred:\n"
for pos, err in errors:
message += f"- Location [{toMm(pos[0])}, {toMm(pos[1])}]\n"
message += textwrap.indent(err, " ")
super().__init__(message)

def identity(x):
return x

Expand Down Expand Up @@ -366,7 +378,7 @@ def polygonToZone(polygon, board):
zone.Outline().AddHole(linestringToKicad(boundary))
return zone

def buildTabs(substrate: Substrate,
def buildTabs(panel: "Panel", substrate: Substrate,
partitionLines: Union[GeometryCollection, LineString],
tabAnnotations: Iterable[TabAnnotation], fillet: KiLength = 0) -> \
Tuple[List[Polygon], List[LineString]]:
Expand All @@ -379,11 +391,17 @@ def buildTabs(substrate: Substrate,
"""
tabs, cuts = [], []
for annotation in tabAnnotations:
t, c = substrate.tab(annotation.origin, annotation.direction,
annotation.width, partitionLines, annotation.maxLength, fillet)
if t is not None:
tabs.append(t)
cuts.append(c)
try:
t, c = substrate.tab(annotation.origin, annotation.direction,
annotation.width, partitionLines, annotation.maxLength, fillet)
if t is not None:
tabs.append(t)
cuts.append(c)
except TabError as e:
panel._renderLines(
[constructArrow(annotation.origin, annotation.direction, fromMm(3), fromMm(1))],
Layer.Margin)
panel.reportError(toKiCADPoint(e.origin), str(e))
return tabs, cuts

def normalizePartitionLineOrientation(line):
Expand Down Expand Up @@ -453,6 +471,8 @@ def __init__(self, panelFilename):
when boards are always associated with a project, you have to pass a
name of the resulting file.
"""
self.errors: List[Tuple[KiPoint, str]] = []

self.filename = panelFilename
self.board = pcbnew.NewBoard(panelFilename)
self.sourcePaths = set() # A set of all board files that were appended to the panel
Expand Down Expand Up @@ -487,6 +507,28 @@ def __init__(self, panelFilename):
self.chamferWidth: Optional[KiLength] = None
self.chamferHeight: Optional[KiLength] = None

def reportError(self, position: KiPoint, message: str) -> None:
"""
Reports a non-fatal error. The error is marked and rendered to the panel
"""
footprint = pcbnew.FootprintLoad(KIKIT_LIB, "Error")
footprint.SetPosition(position)
for x in footprint.GraphicalItems():
if not isinstance(x, pcbnew.PCB_TEXTBOX):
continue
text = x.GetText()
if text == "MESSAGE":
x.SetText(message)
self.board.Add(footprint)

self.errors.append((position, message))

def hasErrors(self) -> bool:
"""
Report if panel has any non-fatal errors presents
"""
return len(self.errors) > 0

def save(self, reconstructArcs: bool=False, refillAllZones: bool=False):
"""
Saves the panel to a file and makes the requested changes to the prl and
Expand Down Expand Up @@ -1306,9 +1348,9 @@ def makeFrame(self, width: KiLength, hspace: KiLength, vspace: KiLength,
minHeight - if the panel doesn't meet this height, it is extended
maxWidth - if the panel doesn't meet this width, TooLargeError is raised
maxWidth - if the panel doesn't meet this width, error is set and marked
maxHeight - if the panel doesn't meet this height, TooLargeHeight is raised
maxHeight - if the panel doesn't meet this height, error is set and marked
"""
frameInnerRect = expandRect(shpBoxToRect(self.boardsBBox()), hspace, vspace)
frameOuterRect = expandRect(frameInnerRect, width)
Expand All @@ -1319,7 +1361,8 @@ def makeFrame(self, width: KiLength, hspace: KiLength, vspace: KiLength,
if maxHeight is not None and frameOuterRect.GetHeight() > maxHeight:
sizeErrors.append(f"Panel height {frameOuterRect.GetHeight() / units.mm} mm exceeds the limit {maxHeight / units.mm} mm")
if len(sizeErrors) > 0:
raise TooLargeError(f"Panel doesn't meet size constraints:\n" + "\n".join(f"- {x}" for x in sizeErrors))
self.reportError(toKiCADPoint(frameOuterRect.GetEnd()),
"Panel doesn't meet size constraints:\n" + "\n".join(f"- {x}" for x in sizeErrors))

if frameOuterRect.GetWidth() < minWidth:
diff = minWidth - frameOuterRect.GetWidth()
Expand Down Expand Up @@ -1363,9 +1406,9 @@ def makeTightFrame(self, width: KiLength, slotwidth: KiLength,
minHeight - if the panel doesn't meet this height, it is extended
maxWidth - if the panel doesn't meet this width, TooLargeError is raised
maxWidth - if the panel doesn't meet this width, error is set
maxHeight - if the panel doesn't meet this height, TooLargeHeight is raised
maxHeight - if the panel doesn't meet this height, error is set
"""
self.makeFrame(width, hspace, vspace, minWidth, minHeight, maxWidth, maxHeight)
boardSlot = GeometryCollection()
Expand All @@ -1380,12 +1423,12 @@ def makeRailsTb(self, thickness: KiLength, minHeight: KiLength = 0,
"""
Adds a rail to top and bottom. You can specify minimal height the panel
has to feature. You can also specify maximal height of the panel. If the
height would be exceeded, TooLargeError is raised.
height would be exceeded, error is set.
"""
minx, miny, maxx, maxy = self.panelBBox()
height = maxy - miny + 2 * thickness
if maxHeight is not None and height > maxHeight:
raise TooLargeError(f"Panel height {height / units.mm} mm exceeds the limit {maxHeight / units.mm} mm")
self.reportError(toKiCADPoint((maxx, maxy)), f"Panel height {height / units.mm} mm exceeds the limit {maxHeight / units.mm} mm")
if height < minHeight:
thickness = (minHeight - maxy + miny) // 2
topRail = box(minx, maxy, maxx, maxy + thickness)
Expand All @@ -1402,7 +1445,7 @@ def makeRailsLr(self, thickness: KiLength, minWidth: KiLength = 0,
minx, miny, maxx, maxy = self.panelBBox()
width = maxx - minx + 2 * thickness
if maxWidth is not None and width > maxWidth:
raise TooLargeError(f"Panel width {width / units.mm} mm exceeds the limit {maxWidth / units.mm} mm")
self.reportError(toKiCADPoint((maxx, maxy)), f"Panel width {width / units.mm} mm exceeds the limit {maxWidth / units.mm} mm")
if width < minWidth:
thickness = (minWidth - maxx + minx) // 2
leftRail = box(minx - thickness, miny, minx, maxy)
Expand Down Expand Up @@ -1442,7 +1485,7 @@ def makeVCuts(self, cuts, boundCurves=False, offset=fromMm(0)):
"""
Take a list of lines to cut and performs V-CUTS. When boundCurves is
set, approximate curved cuts by a line from the first and last point.
Otherwise, raise an exception.
Otherwise, make an approximate cut and report error.
"""
for cut in cuts:
if len(cut.simplify(SHP_EPSILON).coords) > 2 and not boundCurves:
Expand All @@ -1451,7 +1494,9 @@ def makeVCuts(self, cuts, boundCurves=False, offset=fromMm(0)):
message += "- your tabs hit a curved boundary of your PCB,\n"
message += "- your vertical or horizontal PCB edges are not precisely vertical or horizontal.\n"
message += "Modify the design or accept curve approximation via V-cuts."
raise RuntimeError(message)
self._renderLines([cut], Layer.Margin)
self.reportError(toKiCADPoint(cut[0]), message)
continue
cut = cut.simplify(1).parallel_offset(offset, "left")
start = roundPoint(cut.coords[0])
end = roundPoint(cut.coords[-1])
Expand All @@ -1466,7 +1511,9 @@ def makeVCuts(self, cuts, boundCurves=False, offset=fromMm(0)):
message += "- check that intended edges are truly horizonal or vertical\n"
message += "- check your tab placement if it as expected\n"
message += "You can use layer style of cuts to see them and validate them."
raise RuntimeError(message)
self._renderLines([cut], Layer.Margin)
self.reportError(toKiCADPoint(cut[0]), message)
continue

def makeMouseBites(self, cuts, diameter, spacing, offset=fromMm(0.25),
prolongation=fromMm(0.5)):
Expand Down Expand Up @@ -1655,7 +1702,7 @@ def buildTabsFromAnnotations(self, fillet: KiLength) -> List[LineString]:
"""
tabs, cuts = [], []
for s in self.substrates:
t, c = buildTabs(s, s.partitionLine, s.annotations, fillet)
t, c = buildTabs(self, s, s.partitionLine, s.annotations, fillet)
tabs.extend(t)
cuts.extend(c)
self.boardSubstrate.union(tabs)
Expand Down Expand Up @@ -1868,10 +1915,11 @@ def copperFillNonBoardAreas(self, clearance: KiLength=fromMm(1),
By default, fills top and bottom layer, but you can specify any other
copper layer that is enabled.
"""
_, _, maxx, maxy = self.panelBBox()
if not self.boardSubstrate.isSinglePiece():
raise RuntimeError("The substrate has to be a single piece to fill unused areas")
if not len(layers)>0:
raise RuntimeError("No layers to add copper to")
self.reportError(toKiCADPoint((maxx, maxy)), "The substrate has to be a single piece to fill unused areas")
if len(layers) == 0:
self.reportError(toKiCADPoint((maxx, maxy)), "No layers to add copper to")
increaseZonePriorities(self.board)

zoneArea = self.boardSubstrate.exterior()
Expand Down Expand Up @@ -2213,6 +2261,8 @@ def translate(self, vec):
c += vec[1]
self.setAuxiliaryOrigin(self.getAuxiliaryOrigin() + vec)
self.setGridOrigin(self.getGridOrigin() + vec)
for error in self.errors:
error = (error[0] + vec, error[1])
for drcE in self.drcExclusions:
drcE.position += vec

Expand Down
18 changes: 14 additions & 4 deletions kikit/panelize_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,12 @@ def panelize(input, output, preset, plugin, layout, source, tabs, cuts, framing,
f.write(ki.dumpPreset(preset))
except Exception as e:
import sys
sys.stderr.write("An error occurred: " + str(e) + "\n")
sys.stderr.write("No output files produced\n")
from kikit.panelize import NonFatalErrors
if isinstance(e, NonFatalErrors):
sys.stderr.write(str(e) + "\n")
else:
sys.stderr.write("An error occurred: " + str(e) + "\n")
sys.stderr.write("No output files produced\n")
if isinstance(preset, dict) and preset["debug"]["trace"]:
traceback.print_exc(file=sys.stderr)
sys.exit(1)
Expand All @@ -233,7 +237,7 @@ def doPanelization(input, output, preset, plugins=[]):
handle errors based on the context; e.g., CLI vs GUI
"""
from kikit import panelize_ui_impl as ki
from kikit.panelize import Panel
from kikit.panelize import Panel, NonFatalErrors
from pcbnewTransition.transition import pcbnew
from pcbnewTransition.pcbnew import LoadBoard
from itertools import chain
Expand Down Expand Up @@ -301,6 +305,9 @@ def doPanelization(input, output, preset, plugins=[]):
panel.save(reconstructArcs=preset["post"]["reconstructarcs"],
refillAllZones=preset["post"]["refillzones"])

if panel.hasErrors():
raise NonFatalErrors(panel.errors)


@click.command()
@click.argument("input", type=click.Path(dir_okay=False))
Expand All @@ -326,7 +333,7 @@ def separate(input, output, source, page, debug, keepannotations, preservearcs):
"""
try:
from kikit import panelize_ui_impl as ki
from kikit.panelize import Panel
from kikit.panelize import Panel, NonFatalErrors
from kikit.units import mm
from pcbnewTransition import pcbnew
from pcbnewTransition.pcbnew import LoadBoard, VECTOR2I
Expand Down Expand Up @@ -355,6 +362,9 @@ def separate(input, output, source, page, debug, keepannotations, preservearcs):
ki.positionPanel(preset["page"], panel)

panel.save(reconstructArcs=preservearcs)

if panel.hasErrors():
raise NonFatalErrors(panel.errors)
except Exception as e:
import sys
sys.stderr.write("An error occurred: " + str(e) + "\n")
Expand Down
31 changes: 17 additions & 14 deletions kikit/substrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ def __init__(self, message, point):
super().__init__(message)
self.point = point

class TabError(RuntimeError):
def __init__(self, origin, direction, hints):
self.origin = origin
self.direction = direction
message = "Cannot create tab; possible causes:\n"
for hint in hints:
message += f"- {hint}\n"
super().__init__(message)

class TabFilletError(RuntimeError):
pass

Expand Down Expand Up @@ -823,9 +832,9 @@ def tab(self, origin, direction, width, partitionLine=None,
self.orient()

origin = np.array(origin)
direction = np.around(normalize(direction), 4)
for geom in listGeometries(self.substrates):
try:
direction = np.around(normalize(direction), 4)
sideOriginA = origin + makePerpendicular(direction) * width / 2
sideOriginB = origin - makePerpendicular(direction) * width / 2
boundary = geom.exterior
Expand Down Expand Up @@ -874,19 +883,13 @@ def tab(self, origin, direction, width, partitionLine=None,
except NoIntersectionError as e:
continue
except TabFilletError as e:
message = f"Cannot create fillet for tab: {e}\n"
message += f" Annotation position {self._strPosition(origin)}\n"
message += "This is a bug. Please open an issue and provide the board on which the fillet failed."
raise RuntimeError(message) from None

message = "Cannot create tab:\n"
message += f" Annotation position {self._strPosition(origin)}\n"
message += f" Tab ray origin that failed: {self._strPosition(origin)}\n"
message += "Possible causes:\n"
message += "- too wide tab so it does not hit the board,\n"
message += "- annotation is placed inside the board,\n"
message += "- ray length is not sufficient,\n"
raise RuntimeError(message) from None
raise TabError(origin, direction, ["This is a bug. Please open an issue and provide the board on which the fillet failed."])

raise TabError(origin, direction, [
"too wide tab so it does not hit the board",
"annotation is placed inside the board",
"ray length is not sufficient"
])

def _makeTabFillet(self, tab: Polygon, tabFace: LineString, fillet: KiLength) \
-> Tuple[Polygon, LineString]:
Expand Down

0 comments on commit d1bbcdf

Please sign in to comment.