From c3a8dd0be2754a15fc864f88ec9aab5479f8efda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Mr=C3=A1zek?= Date: Sun, 29 Oct 2023 19:17:26 +0100 Subject: [PATCH] Rework copperfills and add hex copper fill --- docs/panelization/cli.md | 11 +- kikit/panel_features/__init__.py | 1 + kikit/panel_features/baseFeature.py | 8 + kikit/panel_features/copperFill.py | 163 +++++++++++++++++++ kikit/panelize.py | 13 +- kikit/panelize_ui_impl.py | 32 ++-- kikit/panelize_ui_sections.py | 32 +++- kikit/pcbnew_utils.py | 9 + kikit/resources/panelizePresets/default.json | 5 +- 9 files changed, 251 insertions(+), 23 deletions(-) create mode 100644 kikit/panel_features/__init__.py create mode 100644 kikit/panel_features/baseFeature.py create mode 100644 kikit/panel_features/copperFill.py create mode 100644 kikit/pcbnew_utils.py diff --git a/docs/panelization/cli.md b/docs/panelization/cli.md index 3de1d8b6..b48d5331 100644 --- a/docs/panelization/cli.md +++ b/docs/panelization/cli.md @@ -525,12 +525,13 @@ via `width` and `height`. Fill non-board areas of the panel with copper. -**Types**: none, solid, hatched +**Types**: none, solid, hatched, hex **Common options**: - `clearance` - optional extra clearance from the board perimeters. Suitable for, e.g., not filling the tabs with copper. +- `edgeclearance` - specifies clearance between the fill and panel perimeter. - `layers` - comma-separated list of layer to fill. Default top and bottom. You can specify a shortcut `all` to fill all layers. @@ -546,6 +547,14 @@ Use hatch pattern for the fill. - `spacing` - the space between the strokes - `orientation` - the orientation of the strokes +#### Hex + +Use hexagon pattern for the fill. + +- `diameter` – diameter of the hexagons +- `spacing` – space between the hexagons +- `threshold` – a percentage value that will discard fragments smaller than + given threshold ### Post diff --git a/kikit/panel_features/__init__.py b/kikit/panel_features/__init__.py new file mode 100644 index 00000000..8f0e4d17 --- /dev/null +++ b/kikit/panel_features/__init__.py @@ -0,0 +1 @@ +from .baseFeature import PanelFeature diff --git a/kikit/panel_features/baseFeature.py b/kikit/panel_features/baseFeature.py new file mode 100644 index 00000000..6c6a23bd --- /dev/null +++ b/kikit/panel_features/baseFeature.py @@ -0,0 +1,8 @@ +from ..panelize import Panel + +class PanelFeature: + """ + Basic interface for various + """ + def apply(self, panel: Panel) -> None: + raise NotImplementedError("Implementation error: PanelFeature doesn't support applying") diff --git a/kikit/panel_features/copperFill.py b/kikit/panel_features/copperFill.py new file mode 100644 index 00000000..26b0adc3 --- /dev/null +++ b/kikit/panel_features/copperFill.py @@ -0,0 +1,163 @@ +from dataclasses import dataclass, field +from ..substrate import linestringToKicad +from ..defs import Layer +from ..common import KiAngle, KiLength, fromDegrees, fromMm +from ..pcbnew_utils import increaseZonePriorities +from pcbnewTransition import pcbnew +from ..panelize import Panel +from .baseFeature import PanelFeature +from typing import Any, List, Tuple +import numpy as np +from shapely.geometry import ( + Polygon, + MultiPolygon) + +class KiCADCopperFillMixin(PanelFeature): + """ + Build solid infill of non-board areas + """ + def _adjustZoneParameters(self, zone: pcbnew.ZONE): + """ + Allow an inherited class to override KiCAD zone parameters + """ + pass # solid infill does nothing + + def apply(self, panel: Any) -> None: + if not panel.boardSubstrate.isSinglePiece(): + raise RuntimeError( + "The substrate has to be a single piece to fill unused areas" + ) + if not len(self.layers) > 0: + raise RuntimeError("No layers to add copper to") + increaseZonePriorities(panel.board) + + zoneArea = panel.boardSubstrate.substrates.buffer(-self.edgeclearance) + zoneArea = zoneArea.difference(MultiPolygon( + [substrate.exterior().buffer(self.clearance) for substrate in panel.substrates] + )) + + geoms = [zoneArea] if isinstance(zoneArea, Polygon) else zoneArea.geoms + + for g in geoms: + zoneContainer = pcbnew.ZONE(panel.board) + self._adjustZoneParameters(zoneContainer) + zoneContainer.Outline().AddOutline(linestringToKicad(g.exterior)) + for hole in g.interiors: + zoneContainer.Outline().AddHole(linestringToKicad(hole)) + zoneContainer.SetAssignedPriority(0) + + for l in self.layers: + if not panel.board.GetEnabledLayers().Contains(l): + continue + zoneContainer = zoneContainer.Duplicate() + zoneContainer.SetLayer(l) + panel.board.Add(zoneContainer) + panel.zonesToRefill.append(zoneContainer) + + +@dataclass +class SolidCopperFill(KiCADCopperFillMixin): + """ + Build solid infill of non-board areas + """ + clearance: KiLength = field(default_factory=lambda: fromMm(1)) + edgeclearance: KiLength = field(default_factory=lambda: fromMm(1)) + layers: List[Layer] = field(default_factory=lambda: [Layer.F_Cu, Layer.B_Cu]) + + def _adjustZoneParameters(self, zone: pcbnew.ZONE) -> None: + pass # There are no adjustments for solid infill + + +@dataclass +class HatchedCopperFill(KiCADCopperFillMixin): + """ + Build hatched infill of non-board areas + """ + clearance: KiLength = field(default_factory=lambda: fromMm(1)) + edgeclearance: KiLength = field(default_factory=lambda: fromMm(1)) + layers: List[Layer] = field(default_factory=lambda: [Layer.F_Cu, Layer.B_Cu]) + strokeWidth: KiLength = field(default_factory=lambda: fromMm(1)) + strokeSpacing: KiLength = field(default_factory=lambda: fromMm(1)) + orientation: KiAngle = field(default_factory=lambda: fromDegrees(45)) + + def _adjustZoneParameters(self, zoneContainer: pcbnew.ZONE) -> None: + zoneContainer.SetFillMode(pcbnew.ZONE_FILL_MODE_HATCH_PATTERN) + zoneContainer.SetHatchOrientation(self.orientation) + zoneContainer.SetHatchGap(self.strokeSpacing) + zoneContainer.SetHatchThickness(self.strokeWidth) + +@dataclass +class HexCopperFill(PanelFeature): + """ + Build hex infill of non-board areas + """ + clearance: KiLength = field(default_factory=lambda: fromMm(1)) + edgeclearance: KiLength = field(default_factory=lambda: fromMm(1)) + layers: List[Layer] = field(default_factory=lambda: [Layer.F_Cu, Layer.B_Cu]) + diameter: KiLength = field(default_factory=lambda: fromMm(5)) + space: KiLength = field(default_factory=lambda: fromMm(0.5)) + threshold: float = field(default_factory=lambda: 0.15) + + def _buildHexagonsPolygon(self, area: Tuple[float, float, float, float]) -> MultiPolygon: + horizontalSpacing = self.space + np.sqrt(3) / 2 * self.diameter + verticalSpacing = 3 / 4 * self.diameter + np.sqrt(3) / 2 * self.space + + minx, miny, maxx, maxy = area + + maxx += horizontalSpacing + maxy += horizontalSpacing + + hexagons = [] + y = miny + shifted = False + while y <= maxy: + x = minx - (horizontalSpacing / 2 if shifted else 0) + while x <= maxx: + hexagons.append(Polygon([ + (x + self.diameter / 2 * np.cos(np.pi / 6 + i / 3 * np.pi), + y + self.diameter / 2 * np.sin(np.pi / 6 + i / 3 * np.pi)) for i in range(6) + ])) + x += horizontalSpacing + y += verticalSpacing + shifted = not shifted + + return MultiPolygon(hexagons) + + def apply(self, panel: Panel) -> None: + if not panel.boardSubstrate.isSinglePiece(): + raise RuntimeError( + "The substrate has to be a single piece to fill unused areas" + ) + if not len(self.layers) > 0: + raise RuntimeError("No layers to add copper to") + + increaseZonePriorities(panel.board) + + zoneArea = panel.boardSubstrate.substrates.buffer(-self.edgeclearance) + zoneArea = zoneArea.intersection(panel.boardSubstrate.substrates) + zoneArea = zoneArea.difference(MultiPolygon( + [substrate.exterior().buffer(self.clearance) for substrate in panel.substrates] + )) + + hexagons = self._buildHexagonsPolygon(zoneArea.bounds) + hexagons = hexagons.intersection(zoneArea) + + baseHexArea = 3 * np.sqrt(3) * (self.diameter / 2) ** 2 / 2 + + geoms = [hexagons] if isinstance(hexagons, Polygon) else hexagons.geoms + for g in geoms: + if g.area < self.threshold * baseHexArea: + continue + zoneContainer = pcbnew.ZONE(panel.board) + zoneContainer.Outline().AddOutline(linestringToKicad(g.exterior)) + for hole in g.interiors: + zoneContainer.Outline().AddHole(linestringToKicad(hole)) + zoneContainer.SetAssignedPriority(0) + + for l in self.layers: + if not panel.board.GetEnabledLayers().Contains(l): + continue + zoneContainer = zoneContainer.Duplicate() + zoneContainer.SetLayer(l) + panel.board.Add(zoneContainer) + panel.zonesToRefill.append(zoneContainer) diff --git a/kikit/panelize.py b/kikit/panelize.py index c4b71312..82a8c96a 100644 --- a/kikit/panelize.py +++ b/kikit/panelize.py @@ -33,6 +33,7 @@ from kikit.annotations import AnnotationReader, TabAnnotation from kikit.drc import DrcExclusion, readBoardDrcExclusions, serializeExclusion from kikit.units import mm, deg +from kikit.pcbnew_utils import increaseZonePriorities class PanelError(RuntimeError): pass @@ -338,10 +339,6 @@ def isBoardEdge(edge): """ return isinstance(edge, pcbnew.PCB_SHAPE) and edge.GetLayerName() == "Edge.Cuts" -def increaseZonePriorities(board, amount=1): - for zone in board.Zones(): - zone.SetAssignedPriority(zone.GetAssignedPriority() + amount) - def tabSpacing(width, count): """ Given a width of board edge and tab count, return an iterable with tab @@ -1832,6 +1829,8 @@ def copperFillNonBoardAreas(self, clearance: KiLength=fromMm(1), strokeWidth: KiLength=fromMm(1), strokeSpacing: KiLength=fromMm(1), orientation: KiAngle=fromDegrees(45)) -> None: """ + This function is deprecated, please, use panel features instead. + Fill given layers with copper on unused areas of the panel (frame, rails and tabs). You can specify the clearance, if it should be hatched (default is solid) or shape the strokes of hatched pattern. @@ -2221,6 +2220,12 @@ def addPanelDimensions(self, layer: Layer, offset: KiLength) -> None: vDim.SetExtensionOffset(-self.filletSize) self.board.Add(vDim) + def apply(self, feature: Any) -> None: + """ + Apply given feature to the panel + """ + feature.apply(self) + def getFootprintByReference(board, reference): """ diff --git a/kikit/panelize_ui_impl.py b/kikit/panelize_ui_impl.py index 642c374e..613c0ea8 100644 --- a/kikit/panelize_ui_impl.py +++ b/kikit/panelize_ui_impl.py @@ -1,4 +1,5 @@ from kikit import panelize +from kikit.panel_features.copperFill import HatchedCopperFill, HexCopperFill, SolidCopperFill from kikit.panelize_ui import Section, PresetError from kikit.panelize import * from kikit.defs import Layer @@ -579,20 +580,29 @@ def buildCopperfill(preset, panel): if type == "none": return if type == "solid": - panel.copperFillNonBoardAreas( + panel.apply(SolidCopperFill( clearance=preset["clearance"], + edgeclearance=preset["edgeclearance"], layers=preset["layers"], - hatched=False - ) + )) if type == "hatched": - panel.copperFillNonBoardAreas( - clearance=preset["clearance"], - layers=preset["layers"], - hatched=True, - strokeWidth=preset["width"], - strokeSpacing=preset["spacing"], - orientation=preset["orientation"] - ) + panel.apply(HatchedCopperFill( + clearance=preset["clearance"], + edgeclearance=preset["edgeclearance"], + layers=preset["layers"], + strokeWidth=preset["width"], + strokeSpacing=preset["spacing"], + orientation=preset["orientation"] + )) + if type == "hex": + panel.apply(HexCopperFill( + clearance=preset["clearance"], + edgeclearance=preset["edgeclearance"], + layers=preset["layers"], + diameter=preset["diameter"], + space=preset["spacing"], + threshold=preset["threshold"] + )) except KeyError as e: raise PresetError(f"Missing parameter '{e}' in section 'postprocessing'") diff --git a/kikit/panelize_ui_sections.py b/kikit/panelize_ui_sections.py index ce8d26af..7c165d5c 100644 --- a/kikit/panelize_ui_sections.py +++ b/kikit/panelize_ui_sections.py @@ -31,6 +31,16 @@ def __init__(self, *args, **kwargs): def validate(self, x): return readLength(x) +class SPercent(SectionBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def validate(self, x): + x = x.strip() + if not x.endswith("%"): + raise PresetError("Percentage error has to end with %") + return readPercents(x) + class SLengthOrPercent(SectionBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -618,14 +628,17 @@ def ppText(section): COPPERFILL_SECTION = { "type": SChoice( - ["none", "solid", "hatched"], + ["none", "solid", "hatched", "hex"], always(), "Fill non board areas with copper"), "clearance": SLength( - typeIn(["solid", "hatched"]), + typeIn(["solid", "hatched", "hex"]), + "Clearance between the fill and boards"), + "edgeclearance": SLength( + typeIn(["solid", "hatched", "hex"]), "Clearance between the fill and boards"), "layers": SLayerList( - typeIn(["solid", "hatched"]), + typeIn(["solid", "hatched", "hex"]), "Specify which layer to fill with copper", { "all": Layer.allCu() @@ -634,11 +647,18 @@ def ppText(section): typeIn(["hatched"]), "Width of hatch strokes"), "spacing": SLength( - typeIn(["hatched"]), - "Spacing of hatch strokes"), + typeIn(["hatched", "hex"]), + "Spacing of hatch strokes or hexagons"), "orientation": SAngle( typeIn(["hatched"]), - "Orientation of the strokes" + "Orientation of the strokes"), + "diameter": SLength( + typeIn(["hex"]), + "Diameter of hexagons" + ), + "threshold": SPercent( + typeIn(["hex"]), + "Remove fragments smaller than threshold" ) } diff --git a/kikit/pcbnew_utils.py b/kikit/pcbnew_utils.py new file mode 100644 index 00000000..1be25dff --- /dev/null +++ b/kikit/pcbnew_utils.py @@ -0,0 +1,9 @@ +from pcbnewTransition import pcbnew + + +def increaseZonePriorities(board: pcbnew.BOARD, amount: int = 1): + """ + Given a board, increase priority of all zones by given amount + """ + for zone in board.Zones(): + zone.SetAssignedPriority(zone.GetAssignedPriority() + amount) diff --git a/kikit/resources/panelizePresets/default.json b/kikit/resources/panelizePresets/default.json index add2a52c..5b79aa90 100644 --- a/kikit/resources/panelizePresets/default.json +++ b/kikit/resources/panelizePresets/default.json @@ -161,10 +161,13 @@ "copperfill": { "type": "none", "clearance": "0.5mm", + "edgeclearance": "0.5mm", "layers": "F.Cu,B.Cu", "width": "1mm", + "diameter": "5mm", "spacing": "1mm", - "orientation": "45deg" + "orientation": "45deg", + "threshold": "15%" }, "post": { "type": "auto",