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 00000000..29f8eb8e Binary files /dev/null and b/tests/reference/kibom-variant_3-F_Fab.pdf differ diff --git a/tests/test_plot/test_print_pcb.py b/tests/test_plot/test_print_pcb.py index 5f36e7b4..71655ed0 100644 --- a/tests/test_plot/test_print_pcb.py +++ b/tests/test_plot/test_print_pcb.py @@ -39,3 +39,14 @@ def test_print_pcb_refill(): ctx.expect_out_file(PDF_FILE_B) ctx.compare_image(PDF_FILE_B) + + +def test_print_variant_1(): + prj = 'kibom-variant_3' + ctx = context.TestContext('test_print_variant_1', prj, 'print_pcb_variant_1', '') + ctx.run() + # Check all outputs are there + fname = prj+'-F_Fab.pdf' + ctx.expect_out_file(fname) + ctx.compare_pdf(fname) + ctx.clean_up() diff --git a/tests/utils/context.py b/tests/utils/context.py index a873bc6d..251a9f9a 100644 --- a/tests/utils/context.py +++ b/tests/utils/context.py @@ -337,7 +337,7 @@ class TestContext(object): image, reference, # Avoid the part where KiCad version is printed - '-crop', '100%x92%+0+0', '+repage', + '-crop', '100%x88%+0+0', '+repage', '-colorspace', 'RGB', self.get_out_path(diff)] logging.debug('Comparing images with: '+str(cmd)) diff --git a/tests/yaml_samples/print_pcb_variant_1.kibot.yaml b/tests/yaml_samples/print_pcb_variant_1.kibot.yaml new file mode 100644 index 00000000..38cacd68 --- /dev/null +++ b/tests/yaml_samples/print_pcb_variant_1.kibot.yaml @@ -0,0 +1,17 @@ +# Example KiBot config file +kibot: + version: 1 + +variants: + - name: 'default' + comment: 'Default variant' + type: ibom + variants_blacklist: T2,T3 + +outputs: + - name: 'pdf_default' + comment: "PCB print w/variant" + type: pdf_pcb_print + options: + variant: default + layers: F.Fab