From 9d606fa53b4d2c0752b303441623739b41253129 Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Wed, 13 Apr 2022 13:51:03 -0300 Subject: [PATCH] Added colored pads and vias to pcb_print - The vias are more "realistic", we avoid showing holes on layers that aren't drilled, but we mark the via. So you know were is. --- README.md | 4 + docs/samples/generic_plot.kibot.yaml | 8 ++ kibot/kicad/color_theme.py | 14 ++- kibot/out_pcb_print.py | 160 ++++++++++++++++++++++++++- 4 files changed, 180 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 63a23f23..d655bdec 100644 --- a/README.md +++ b/README.md @@ -1538,6 +1538,8 @@ Next time you need this list just use an alias, like this: - `color_theme`: [string='_builtin_classic'] Selects the color theme. Only applies to KiCad 6. To use the KiCad 6 default colors select `_builtin_default`. Usually user colors are stored as `user`, but you can give it another name. + - `colored_pads`: [boolean=true] Plot through-hole in a different color. Like KiCad GUI does. + - `colored_vias`: [boolean=true] Plot vias in a different color. Like KiCad GUI does. - `dnf_filter`: [string|list(string)='_none'] Name of the filter to mark components as not fitted. A short-cut to use for simple cases where a variant is an overkill. - `drill_marks`: [string='full'] What to use to indicate the drill places, can be none, small or full (for real scale). @@ -1549,6 +1551,7 @@ Next time you need this list just use an alias, like this: - `hide_excluded`: [boolean=false] Hide components in the Fab layer that are marked as excluded by a variant. - `output`: [string='%f-%i%I%v.%x'] Filename for the output (%i=assembly, %x=pdf)/(%i=assembly_page_NN, %x=svg). Affected by global options. - *output_name*: Alias for output. + - `pad_color`: [string=''] Color used for `colored_pads`. - `pages`: [list(dict)] List of pages to include in the output document. Each page contains one or more layers of the PCB. * Valid keys: @@ -1580,6 +1583,7 @@ Next time you need this list just use an alias, like this: - `title`: [string=''] Text used to replace the sheet title. %VALUE expansions are allowed. If it starts with `+` the text is concatenated. - `variant`: [string=''] Board variant to apply. + - `via_color`: [string=''] Color used for `colored_vias`. - `output_id`: [string=''] Text to use for the %I expansion content. To differentiate variations of this output. - `run_by_default`: [boolean=true] When enabled this output will be created when no specific outputs are requested. diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index dfc2d226..d367e3b5 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -964,6 +964,10 @@ outputs: # To use the KiCad 6 default colors select `_builtin_default`. # Usually user colors are stored as `user`, but you can give it another name color_theme: '_builtin_classic' + # [boolean=true] Plot through-hole in a different color. Like KiCad GUI does + colored_pads: true + # [boolean=true] Plot vias in a different color. Like KiCad GUI does + colored_vias: true # [string|list(string)='_none'] 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: '_none' @@ -981,6 +985,8 @@ outputs: # [string='%f-%i%I%v.%x'] Filename for the output (%i=assembly, %x=pdf)/(%i=assembly_page_NN, %x=svg). Affected by global options output: '%f-%i%I%v.%x' # `output_name` is an alias for `output` + # [string=''] Color used for `colored_pads` + pad_color: '' # [list(dict)] List of pages to include in the output document. # Each page contains one or more layers of the PCB pages: @@ -1036,6 +1042,8 @@ outputs: title: '' # [string=''] Board variant to apply variant: '' + # [string=''] Color used for `colored_vias` + via_color: '' # PcbDraw - Beautiful 2D PCB render: # Uses configurable colors. # Can also render the components if the 2D models are available diff --git a/kibot/kicad/color_theme.py b/kibot/kicad/color_theme.py index 713f6447..8c5acd4b 100644 --- a/kibot/kicad/color_theme.py +++ b/kibot/kicad/color_theme.py @@ -26,6 +26,11 @@ KI6_KI5 = {'b_adhesive': 'b_adhes', 'user_eco2': 'eco2_user', 'b_courtyard': 'b_crtyd', 'f_courtyard': 'f_crtyd'} +BOARD_COLORS = {'worksheet': 'pcb_frame', + 'pad_through_hole': 'pad_through_hole', + 'via_through': 'via_through', + 'via_blind_buried': 'via_blind_buried', + 'via_micro': 'via_micro'} CACHE = {} @@ -33,6 +38,8 @@ class KiCadColors(object): def __init__(self): self.layer_id2color = {} self.pcb_frame = "#480000" + self.pad_through_hole = "#C2C200" + self.via_through = "#C2C2C2" def parse_color(val): @@ -95,8 +102,9 @@ def load_color_theme(name): logger.warning(W_WRONGCOLOR+"The `{}` theme doesn't define a color for the {} layer".format(name, c_name_ori)) if extra_debug: logger.debug('- Color for layer {} ({}): {}'.format(c_name_ori, id, cl[id])) - # Title block and frame color - if 'worksheet' in board: - c.pcb_frame = parse_color(board['worksheet']) + # Other colors (Title block and frame color, vias, etc.) + for color, member in BOARD_COLORS.items(): + if color in board: + setattr(c, member, parse_color(board[color])) CACHE[fn] = c return c diff --git a/kibot/out_pcb_print.py b/kibot/out_pcb_print.py index 4aaa3d1f..bff1d982 100644 --- a/kibot/out_pcb_print.py +++ b/kibot/out_pcb_print.py @@ -8,7 +8,7 @@ # Note: Original code released as Public Domain import os import subprocess -from pcbnew import PLOT_CONTROLLER, FromMM, PLOT_FORMAT_SVG +from pcbnew import PLOT_CONTROLLER, FromMM, PLOT_FORMAT_SVG, F_Cu, B_Cu, wxSize, IsCopperLayer from shutil import rmtree, which from tempfile import mkdtemp from .svgutils.transform import fromstring @@ -28,10 +28,12 @@ from . import log logger = log.get_logger() SVG2PDF = 'rsvg-convert' PDF2PS = 'pdf2ps' +VIATYPE_THROUGH = 3 +VIATYPE_BLIND_BURIED = 2 +VIATYPE_MICROVIA = 1 # - Use PyPDF2 for pdfunite -# - Implement pad, vias, etc colors # - Analyze KiCad 6 long delay # - Manually draw the frame @@ -272,6 +274,10 @@ class PagesOptions(Optionable): class PCB_PrintOptions(VariantOptions): # Mappings to KiCad config values. They should be the same used in drill_marks.py _drill_marks_map = {'none': 0, 'small': 1, 'full': 2} + _pad_colors = {'pad_color': 'pad_through_hole', + 'via_color': 'via_through', + 'micro_via_color': 'via_micro', + 'blind_via_color': 'via_blind_buried'} def __init__(self): with document: @@ -304,6 +310,18 @@ class PCB_PrintOptions(VariantOptions): Note that for PS you need `ghostscript` which isn't part of the default docker images """ self.png_width = 1280 """ Width of the PNG in pixels """ + self.colored_pads = True + """ Plot through-hole in a different color. Like KiCad GUI does """ + self.pad_color = '' + """ Color used for `colored_pads` """ + self.colored_vias = True + """ Plot vias in a different color. Like KiCad GUI does """ + self.via_color = '' + """ Color used for through-hole `colored_vias` """ + self.micro_via_color = '' + """ Color used for micro `colored_vias` """ + self.blind_via_color = '' + """ Color used for blind/buried `colored_vias` """ super().__init__() self._expand_id = 'assembly' @@ -335,6 +353,11 @@ class PCB_PrintOptions(VariantOptions): la.color = "#000000" self._drill_marks = PCB_PrintOptions._drill_marks_map[self._drill_marks] self._expand_ext = self.format.lower() + for member, color in self._pad_colors.items(): + if getattr(self, member): + self.validate_color(member) + else: + setattr(self, member, getattr(self._color_theme, color)) def filter_components(self): if not self._comps: @@ -426,6 +449,129 @@ class PCB_PrintOptions(VariantOptions): os.remove(video_name) patch_svg_file(output, remove_bkg=True) + def plot_pads(self, la, pc, p, filelist): + id = la._id + logger.debug('- Plotting pads for layer {} ({})'.format(la.layer, id)) + # Make invisible anything but through-hole pads + tmp_layer = GS.board.GetLayerID(GS.work_layer) + moved = [] + resized = [] + vias = [] + size_0 = wxSize(0, 0) + for m in GS.get_modules(): + for gi in m.GraphicalItems(): + if gi.GetLayer() == id: + gi.SetLayer(tmp_layer) + moved.append(gi) + for pad in m.Pads(): + dr = pad.GetDrillSize() + if dr.x: + continue + resized.append((pad, pad.GetSize())) + pad.SetSize(size_0) + for e in GS.board.GetDrawings(): + if e.GetLayer() == id: + e.SetLayer(tmp_layer) + moved.append(e) + for e in list(GS.board.Zones()): + if e.GetLayer() == id: + e.SetLayer(tmp_layer) + moved.append(e) + via_type = 'VIA' if GS.ki5() else 'PCB_VIA' + for e in GS.board.GetTracks(): + if e.GetClass() == via_type: + vias.append((e, e.GetDrill(), e.GetWidth())) + e.SetDrill(0) + e.SetWidth(0) + else: + e.SetLayer(tmp_layer) + moved.append(e) + # Plot the layer + # pc.SetLayer(id) already selected + suffix = la.suffix+'_pads' + pc.OpenPlotfile(suffix, PLOT_FORMAT_SVG, p.sheet) + pc.PlotLayer() + # Restore everything + for e in moved: + e.SetLayer(id) + for (pad, size) in resized: + pad.SetSize(size) + for (via, drill, width) in vias: + via.SetDrill(drill) + via.SetWidth(width) + # Add it to the list + filelist.append((GS.pcb_basename+"-"+suffix+".svg", self.pad_color)) + + def plot_vias(self, la, pc, p, filelist, via_t, via_c): + id = la._id + logger.debug('- Plotting vias for layer {} ({})'.format(la.layer, id)) + # Make invisible anything but vias + tmp_layer = GS.board.GetLayerID(GS.work_layer) + moved = [] + resized = [] + vias = [] + size_0 = wxSize(0, 0) + for m in GS.get_modules(): + for gi in m.GraphicalItems(): + if gi.GetLayer() == id: + gi.SetLayer(tmp_layer) + moved.append(gi) + for pad in m.Pads(): + resized.append((pad, pad.GetSize())) + pad.SetSize(size_0) + for e in GS.board.GetDrawings(): + if e.GetLayer() == id: + e.SetLayer(tmp_layer) + moved.append(e) + for e in list(GS.board.Zones()): + if e.GetLayer() == id: + e.SetLayer(tmp_layer) + moved.append(e) + via_type = 'VIA' if GS.ki5() else 'PCB_VIA' + for e in GS.board.GetTracks(): + if e.GetClass() == via_type: + if e.GetViaType() == via_t: + # Include it, but ... + if not e.IsOnLayer(id): + # This is a via that doesn't drill this layer + # Lamentably KiCad will draw a drill here + # So we create a "patch" for the hole + top = e.TopLayer() + bottom = e.BottomLayer() + w = e.GetWidth() + d = e.GetDrill() + vias.append((e, d, w, top, bottom)) + e.SetWidth(d) + e.SetDrill(1) + e.SetTopLayer(F_Cu) + e.SetBottomLayer(B_Cu) + else: + top = e.TopLayer() + bottom = e.BottomLayer() + w = e.GetWidth() + d = e.GetDrill() + vias.append((e, d, w, top, bottom)) + e.SetWidth(0) + else: + e.SetLayer(tmp_layer) + moved.append(e) + # Plot the layer + suffix = la.suffix+'_vias_'+str(via_t) + pc.OpenPlotfile(suffix, PLOT_FORMAT_SVG, p.sheet) + pc.PlotLayer() + # Restore everything + for e in moved: + e.SetLayer(id) + for (pad, size) in resized: + pad.SetSize(size) + for (via, drill, width, top, bottom) in vias: + via.SetDrill(drill) + via.SetWidth(width) + via.SetTopLayer(top) + via.SetBottomLayer(bottom) + # Add it to the list + filelist.append((GS.pcb_basename+"-"+suffix+".svg", via_c)) + def generate_output(self, output): if self.format != 'SVG' and which(SVG2PDF) is None: logger.error('`{}` not installed. Install `librsvg2-bin` or equivalent'.format(SVG2PDF)) @@ -445,7 +591,6 @@ class PCB_PrintOptions(VariantOptions): po.SetExcludeEdgeLayer(True) # We plot it separately po.SetUseAuxOrigin(False) po.SetAutoScale(False) - po.SetDrillMarksType(self._drill_marks) # Generate the output pages = [] for n, p in enumerate(self.pages): @@ -466,10 +611,19 @@ class PCB_PrintOptions(VariantOptions): po.SetPlotReference(la.plot_footprint_refs) po.SetPlotValue(la.plot_footprint_values) po.SetPlotInvisibleText(la.force_plot_invisible_refs_vals) + # Avoid holes on non-copper layers + po.SetDrillMarksType(self._drill_marks if IsCopperLayer(id) else 0) pc.SetLayer(id) pc.OpenPlotfile(la.suffix, PLOT_FORMAT_SVG, p.sheet) pc.PlotLayer() filelist.append((GS.pcb_basename+"-"+la.suffix+".svg", la.color)) + if id >= F_Cu and id <= B_Cu: + if self.colored_pads: + self.plot_pads(la, pc, p, filelist) + if self.colored_vias: + self.plot_vias(la, pc, p, filelist, VIATYPE_THROUGH, self.via_color) + self.plot_vias(la, pc, p, filelist, VIATYPE_BLIND_BURIED, self.blind_via_color) + self.plot_vias(la, pc, p, filelist, VIATYPE_MICROVIA, self.micro_via_color) # 2) Plot the frame using an empty layer and 1.0 scale if self.plot_sheet_reference: logger.debug('- Plotting the frame')