From 23e46df1c503201573a6ff4d727c151aeb3d3819 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Mon, 7 Sep 2020 19:26:16 -0300 Subject: [PATCH] Added variants support to the PCB print (PDF) Needs some adjustement, but is working. --- CHANGELOG.md | 4 +- Makefile | 1 + README.md | 3 + docs/samples/generic_plot.kibot.yaml | 5 + kibot/misc.py | 22 +++++ kibot/out_any_layer.py | 24 +---- kibot/out_pdf_pcb_print.py | 87 +++++++++++++++++- tests/reference/kibom-variant_3-F_Fab.pdf | Bin 0 -> 8996 bytes tests/test_plot/test_print_pcb.py | 11 +++ tests/utils/context.py | 2 +- .../print_pcb_variant_1.kibot.yaml | 17 ++++ 11 files changed, 146 insertions(+), 30 deletions(-) create mode 100644 tests/reference/kibom-variant_3-F_Fab.pdf create mode 100644 tests/yaml_samples/print_pcb_variant_1.kibot.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 216b04bf..e786f0ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Marking components as "Do Not Fit" - Marking components as "Do Not Change" - The internal BoM format supports KiBoM and IBoM style variants -- Schematic print to PDF/SVG support for variants. Not fitted components are - crossed. +- Schematic/PCB print to PDF/SVG support for variants. Not fitted components + are crossed. - Position (Pick & Place) support for variants. - All plot formats (gerber, pdf, svg, etc.) support for variants: - Pads removed from *.Paste diff --git a/Makefile b/Makefile index 888d2f6f..67595c10 100644 --- a/Makefile +++ b/Makefile @@ -115,6 +115,7 @@ gen_ref: src/kibot -b tests/board_samples/kibom-variant_4.kicad_pcb -c tests/yaml_samples/pdf_variant_1.kibot.yaml -d $(REFDIR) src/kibot -b tests/board_samples/kibom-variant_3.kicad_pcb -c tests/yaml_samples/pcbdraw_variant_1.kibot.yaml -d $(REFDIR) src/kibot -b tests/board_samples/kibom-variant_3.kicad_pcb -c tests/yaml_samples/pcbdraw_variant_2.kibot.yaml -d $(REFDIR) + src/kibot -b tests/board_samples/kibom-variant_3.kicad_pcb -c tests/yaml_samples/print_pcb_variant_1.kibot.yaml -d $(REFDIR) cp -a $(REFILL).ok $(REFILL) doc: diff --git a/README.md b/README.md index 70c8bf67..fd10ddd1 100644 --- a/README.md +++ b/README.md @@ -831,8 +831,11 @@ Next time you need this list just use an alias, like this: - `name`: [string=''] Used to identify this particular output definition. - `options`: [dict] Options for the `pdf_pcb_print` output. * Valid keys: + - `dnf_filter`: [string|list(string)=''] Name of the filter to mark components as not fitted. + A short-cut to use for simple cases where a variant is an overkill. - `output`: [string='%f-%i%v.%x'] filename for the output PDF (%i=layers, %x=pdf). Affected by global options. - *output_name*: Alias for output. + - `variant`: [string=''] Board variant to apply. * PDF Schematic Print (Portable Document Format) * Type: `pdf_sch_print` diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index 3ad78c86..7e873a1e 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -648,9 +648,14 @@ outputs: type: 'pdf_pcb_print' dir: 'Example/pdf_pcb_print_dir' options: + # [string|list(string)=''] Name of the filter to mark components as not fitted. + # A short-cut to use for simple cases where a variant is an overkill + dnf_filter: '' # [string='%f-%i%v.%x'] filename for the output PDF (%i=layers, %x=pdf). Affected by global options output: '%f-%i%v.%x' # `output_name` is an alias for `output` + # [string=''] Board variant to apply + variant: '' layers: all # PDF Schematic Print (Portable Document Format): diff --git a/kibot/misc.py b/kibot/misc.py index 5e6d39a2..52ff019d 100644 --- a/kibot/misc.py +++ b/kibot/misc.py @@ -77,3 +77,25 @@ DNC = { "no change": 1, "fixed": 1 } + + +class Rect(object): + """ What KiCad returns isn't a real wxWidget's wxRect. + Here I add what I really need """ + def __init__(self): + self.x1 = None + self.y1 = None + self.x2 = None + self.y2 = None + + def Union(self, wxRect): + if self.x1 is None: + self.x1 = wxRect.x + self.y1 = wxRect.y + self.x2 = wxRect.x+wxRect.width + self.y2 = wxRect.y+wxRect.height + else: + self.x1 = min(self.x1, wxRect.x) + self.y1 = min(self.y1, wxRect.y) + self.x2 = max(self.x2, wxRect.x+wxRect.width) + self.y2 = max(self.y2, wxRect.y+wxRect.height) diff --git a/kibot/out_any_layer.py b/kibot/out_any_layer.py index 7496dcc8..9d3a63c4 100644 --- a/kibot/out_any_layer.py +++ b/kibot/out_any_layer.py @@ -11,7 +11,7 @@ from .out_base import (BaseOutput) from .error import (PlotError, KiPlotConfigurationError) from .layer import Layer from .gs import GS -from .misc import UI_VIRTUAL +from .misc import UI_VIRTUAL, Rect from .out_base import VariantOptions from .macros import macros, document # noqa: F401 from . import log @@ -19,28 +19,6 @@ from . import log logger = log.get_logger(__name__) -class Rect(object): - """ What KiCad returns isn't a real wxWidget's wxRect. - Here I add what I really need """ - def __init__(self): - self.x1 = None - self.y1 = None - self.x2 = None - self.y2 = None - - def Union(self, wxRect): - if self.x1 is None: - self.x1 = wxRect.x - self.y1 = wxRect.y - self.x2 = wxRect.x+wxRect.width - self.y2 = wxRect.y+wxRect.height - else: - self.x1 = min(self.x1, wxRect.x) - self.y1 = min(self.y1, wxRect.y) - self.x2 = max(self.x2, wxRect.x+wxRect.width) - self.y2 = max(self.y2, wxRect.y+wxRect.height) - - class AnyLayerOptions(VariantOptions): """ Base class for: DXF, Gerber, HPGL, PDF, PS and SVG """ def __init__(self): diff --git a/kibot/out_pdf_pcb_print.py b/kibot/out_pdf_pcb_print.py index 314bb222..eeebd57d 100644 --- a/kibot/out_pdf_pcb_print.py +++ b/kibot/out_pdf_pcb_print.py @@ -3,12 +3,15 @@ # Copyright (c) 2020 Instituto Nacional de TecnologĂ­a Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) +import os +from tempfile import NamedTemporaryFile +from pcbnew import EDGE_MODULE, wxPoint from .pre_base import BasePreFlight from .error import (KiPlotConfigurationError) from .gs import (GS) from .kiplot import check_script, exec_with_retry -from .misc import (CMD_PCBNEW_PRINT_LAYERS, URL_PCBNEW_PRINT_LAYERS, PDF_PCB_PRINT) -from .optionable import BaseOptions +from .misc import (CMD_PCBNEW_PRINT_LAYERS, URL_PCBNEW_PRINT_LAYERS, PDF_PCB_PRINT, Rect, UI_VIRTUAL) +from .out_base import VariantOptions from .macros import macros, document, output_class # noqa: F401 from .layer import Layer from . import log @@ -16,7 +19,7 @@ from . import log logger = log.get_logger(__name__) -class PDF_Pcb_PrintOptions(BaseOptions): +class PDF_Pcb_PrintOptions(VariantOptions): def __init__(self): with document: self.output = GS.def_global_output @@ -25,7 +28,79 @@ class PDF_Pcb_PrintOptions(BaseOptions): """ {output} """ super().__init__() + @staticmethod + def cross_module(m, rect, layer): + seg1 = EDGE_MODULE(m) + seg1.SetWidth(120000) + seg1.SetStart(wxPoint(rect.x1, rect.y1)) + seg1.SetEnd(wxPoint(rect.x2, rect.y2)) + seg1.SetLayer(layer) + seg1.SetLocalCoord() # Update the local coordinates + m.Add(seg1) + seg2 = EDGE_MODULE(m) + seg2.SetWidth(120000) + seg2.SetStart(wxPoint(rect.x1, rect.y2)) + seg2.SetEnd(wxPoint(rect.x2, rect.y1)) + seg2.SetLayer(layer) + seg2.SetLocalCoord() # Update the local coordinates + m.Add(seg2) + return [seg1, seg2] + + def filter_components(self, board): + if not self._comps: + return GS.pcb_file + comps_hash = self.get_refs_hash() + # Cross the affected components + ffab = board.GetLayerID('F.Fab') + bfab = board.GetLayerID('B.Fab') + extra_ffab_lines = [] + extra_bfab_lines = [] + for m in board.GetModules(): + ref = m.GetReference() + # Rectangle containing the drawings, no text + frect = Rect() + brect = Rect() + c = comps_hash.get(ref, None) + if (c and not c.fitted) and m.GetAttributes() != UI_VIRTUAL: + # Meassure the component BBox (only graphics) + for gi in m.GraphicalItems(): + if gi.GetClass() == 'MGRAPHIC': + l_gi = gi.GetLayer() + if l_gi == ffab: + frect.Union(gi.GetBoundingBox().getWxRect()) + if l_gi == bfab: + brect.Union(gi.GetBoundingBox().getWxRect()) + # Cross the graphics in *.Fab + if frect.x1 is not None: + extra_ffab_lines.append(self.cross_module(m, frect, ffab)) + else: + extra_ffab_lines.append(None) + if brect.x1 is not None: + extra_bfab_lines.append(self.cross_module(m, brect, bfab)) + else: + extra_bfab_lines.append(None) + # Save the PCB to a temporal file + with NamedTemporaryFile(mode='w', suffix='.kicad_pcb', delete=False) as f: + fname = f.name + logger.debug('Storing filtered PCB to `{}`'.format(fname)) + GS.board.Save(fname) + # Undo the drawings + for m in GS.board.GetModules(): + ref = m.GetReference() + c = comps_hash.get(ref, None) + if (c and not c.fitted) and m.GetAttributes() != UI_VIRTUAL: + restore = extra_ffab_lines.pop(0) + if restore: + for line in restore: + m.Remove(line) + restore = extra_bfab_lines.pop(0) + if restore: + for line in restore: + m.Remove(line) + return fname + def run(self, output_dir, board, layers): + super().run(board, layers) check_script(CMD_PCBNEW_PRINT_LAYERS, URL_PCBNEW_PRINT_LAYERS, '1.4.1') layers = Layer.solve(layers) # Output file name @@ -34,7 +109,8 @@ class PDF_Pcb_PrintOptions(BaseOptions): cmd = [CMD_PCBNEW_PRINT_LAYERS, 'export', '--output_name', output] if BasePreFlight.get_option('check_zone_fills'): cmd.append('-f') - cmd.extend([GS.pcb_file, output_dir]) + board_name = self.filter_components(board) + cmd.extend([board_name, output_dir]) if GS.debug_enabled: cmd.insert(1, '-vv') cmd.insert(1, '-r') @@ -42,6 +118,9 @@ class PDF_Pcb_PrintOptions(BaseOptions): cmd.extend([la.layer for la in layers]) # Execute it ret = exec_with_retry(cmd) + # Remove the temporal PCB + if board_name != GS.pcb_file: + os.remove(board_name) if ret: # pragma: no cover # We check all the arguments, we even load the PCB # A fail here isn't easy to reproduce diff --git a/tests/reference/kibom-variant_3-F_Fab.pdf b/tests/reference/kibom-variant_3-F_Fab.pdf new file mode 100644 index 0000000000000000000000000000000000000000..29f8eb8e8c13c0bf5f87da3a0a0c2b2448df888d GIT binary patch literal 8996 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