KiBot/kibot/out_any_layer.py

331 lines
15 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (c) 2020-2023 Salvador E. Tropea
# Copyright (c) 2020-2023 Instituto Nacional de Tecnología Industrial
# Copyright (c) 2018 John Beard
# License: GPL-3.0
# Project: KiBot (formerly KiPlot)
# Adapted from: https://github.com/johnbeard/kiplot
import os
import re
from pcbnew import (GERBER_JOBFILE_WRITER, PLOT_CONTROLLER, IsCopperLayer, F_Cu, B_Cu, Edge_Cuts, PLOT_FORMAT_HPGL,
PLOT_FORMAT_GERBER, PLOT_FORMAT_POST, PLOT_FORMAT_DXF, PLOT_FORMAT_PDF, PLOT_FORMAT_SVG, LSEQ, LSET)
from .optionable import Optionable
from .out_base import BaseOutput, VariantOptions
from .error import PlotError, KiPlotConfigurationError
from .layer import Layer
from .gs import GS
from .misc import W_NOLAYER, KICAD_VERSION_7_0_1, MISSING_TOOL, AUTO_SCALE
from .macros import macros, document # noqa: F401
from . import log
logger = log.get_logger()
FORMAT_EXTENSIONS = {PLOT_FORMAT_HPGL: 'plt',
PLOT_FORMAT_GERBER: 'gbr',
PLOT_FORMAT_POST: 'ps',
PLOT_FORMAT_DXF: 'dxf',
PLOT_FORMAT_PDF: 'pdf',
PLOT_FORMAT_SVG: 'svg'}
class CustomReport(Optionable):
def __init__(self):
super().__init__()
with document:
self.output = 'Custom_report.txt'
""" File name for the custom report """
self.content = ''
""" Content for the report. Use ``${basename}`` for the project name without extension.
Use ``${filename(LAYER)}`` for the file corresponding to LAYER """
class AnyLayerOptions(VariantOptions):
""" Base class for: DXF, Gerber, HPGL, PDF, PS and SVG """
def __init__(self):
with document:
self.exclude_edge_layer = True
""" Do not include the PCB edge layer """
self.exclude_pads_from_silkscreen = False
""" Do not plot the component pads in the silk screen (KiCad 5.x only) """
self.plot_sheet_reference = False
""" *Include the frame and title block. Only available for KiCad 6+ and you get a poor result
(i.e. always the default worksheet style, also problems expanding text variables).
The `pcb_print` output can do a better job for PDF, SVG, PS, EPS and PNG outputs """
self.plot_footprint_refs = True
""" Include the footprint references """
self.plot_footprint_values = True
""" Include the footprint values """
self.force_plot_invisible_refs_vals = False
""" Include references and values even when they are marked as invisible """
self.output = GS.def_global_output
""" *Output file name, the default KiCad name if empty.
IMPORTANT! KiCad will always create the file using its own name and then we can rename it.
For this reason you must avoid generating two variants at the same directory when one of
them uses the default KiCad name """
self.tent_vias = True
""" Cover the vias """
self.uppercase_extensions = False
""" Use uppercase names for the extensions """
self.inner_extension_pattern = ''
""" Used to change the Protel style extensions for inner layers.
The replacement pattern can contain %n for the inner layer number and %N for the layer number.
Example '.g%n' """
self.edge_cut_extension = ''
""" Used to configure the edge cuts layer extension for Protel mode. Include the dot """
self.custom_reports = CustomReport
""" [list(dict)] A list of customized reports for the manufacturer """
self.sketch_pads_on_fab_layers = False
r""" Draw only the outline of the pads on the \*.Fab layers (KiCad 6+) """
self.sketch_pad_line_width = 0.1
""" Line width for the sketched pads [mm], see `sketch_pads_on_fab_layers` (KiCad 6+)
Note that this value is currently ignored by KiCad (6.0.9) """
self.scaling = 1
""" *Scale factor (0 means autoscaling) """
self.individual_page_scaling = True
""" Tell KiCad to apply the scaling for each layer as a separated entity.
Disabling it the pages are coherent and can be superposed """
super().__init__()
def config(self, parent):
super().config(parent)
if isinstance(self.custom_reports, type):
self.custom_reports = []
self.sketch_pad_line_width = GS.from_mm(self.sketch_pad_line_width)
def _configure_plot_ctrl(self, po, output_dir):
logger.debug("Configuring plot controller for output")
po.SetOutputDirectory(output_dir)
po.SetPlotFrameRef(self.plot_sheet_reference and (not GS.ki5))
po.SetPlotReference(self.plot_footprint_refs)
po.SetPlotValue(self.plot_footprint_values)
po.SetPlotInvisibleText(self.force_plot_invisible_refs_vals)
# Edge layer included or not
GS.SetExcludeEdgeLayer(po, self.exclude_edge_layer)
if GS.ki5:
po.SetPlotPadsOnSilkLayer(not self.exclude_pads_from_silkscreen)
else:
po.SetSketchPadsOnFabLayers(self.sketch_pads_on_fab_layers)
po.SetSketchPadLineWidth(self.sketch_pad_line_width)
po.SetPlotViaOnMaskLayer(not self.tent_vias)
# Only useful for gerber outputs
po.SetCreateGerberJobFile(False)
# We'll come back to this on a per-layer basis
po.SetSkipPlotNPTH_Pads(False)
# Scaling/Autoscale
if self._plot_format != PLOT_FORMAT_GERBER:
if self.scaling == AUTO_SCALE:
po.SetAutoScale(True)
po.SetScale(1)
else:
po.SetAutoScale(False)
po.SetScale(self.scaling)
def compute_name(self, k_filename, output_dir, output, id, suffix):
if output:
filename = self.expand_filename(output_dir, output, suffix, os.path.splitext(k_filename)[1][1:])
else:
filename = k_filename
if id > F_Cu and id < B_Cu and self.inner_extension_pattern:
ext = self.inner_extension_pattern
ext = ext.replace('%n', str(id))
ext = ext.replace('%N', str(id+1))
filename = os.path.splitext(filename)[0]+ext
if id == Edge_Cuts and self.edge_cut_extension:
filename = os.path.splitext(filename)[0]+self.edge_cut_extension
if self.uppercase_extensions:
filename = os.path.splitext(filename)[0]+os.path.splitext(filename)[1].upper()
return filename
def plot_layer(self, plot_ctrl, id):
if GS.ki7 and not self.exclude_edge_layer:
# In KiCad 7 this is not an option, but we can plot more than one layer
# Note that this needs KiCad 7.0.1 or newer
seq = LSEQ()
seq.push_back(Edge_Cuts)
seq.push_back(id)
plot_ctrl.PlotLayers(seq)
return
plot_ctrl.PlotLayer()
def run(self, output_dir, layers):
super().run(output_dir)
if GS.ki7 and GS.kicad_version_n < KICAD_VERSION_7_0_1 and not self.exclude_edge_layer:
GS.exit_with_error("Plotting the edge layer is not supported by KiCad 7.0.0\n"
"Please upgrade KiCad to 7.0.1 or newer", MISSING_TOOL)
# Memorize the list of visible layers
old_visible = GS.board.GetVisibleLayers()
# Apply the variants and filters
exclude = self.filter_pcb_components()
# fresh plot controller
plot_ctrl = PLOT_CONTROLLER(GS.board)
# set up plot options for the whole output
po = plot_ctrl.GetPlotOptions()
self._configure_plot_ctrl(po, output_dir)
# Gerber Job files aren't automagically created
# We need to assist KiCad
create_job = po.GetCreateGerberJobFile()
if create_job:
jobfile_writer = GERBER_JOBFILE_WRITER(GS.board)
plot_ctrl.SetColorMode(True)
# Plot every layer in the output
generated = {}
layers = Layer.solve(layers)
# Make visible only the layers we need
# This is very important when scaling, otherwise the results are controlled by the .kicad_prl (See #407)
if self._plot_format != PLOT_FORMAT_GERBER and not self.individual_page_scaling:
vis_layers = LSET()
for la in layers:
vis_layers.addLayer(la._id)
GS.board.SetVisibleLayers(vis_layers)
for la in layers:
suffix = la.suffix
desc = la.description
id = la.id
if not GS.board.IsLayerEnabled(id):
logger.warning(W_NOLAYER+f'Layer "{desc}" ({la.suffix}) isn\'t used')
continue
if self._plot_format != PLOT_FORMAT_GERBER and self.individual_page_scaling:
# Only this layer is visible
vis_layers = LSET()
vis_layers.addLayer(la._id)
GS.board.SetVisibleLayers(vis_layers)
# Set current layer
plot_ctrl.SetLayer(id)
# Skipping NPTH is controlled by whether or not this is
# a copper layer
is_cu = IsCopperLayer(id)
po.SetSkipPlotNPTH_Pads(is_cu)
# Plot single layer to file
logger.debug("Opening plot file for layer `{}` format `{}`".format(la, self._plot_format))
if not plot_ctrl.OpenPlotfile(suffix, self._plot_format, desc):
# Shouldn't happen
raise PlotError("OpenPlotfile failed!") # pragma: no cover (Internal)
# Compute the current file name and the one we want
k_filename = plot_ctrl.GetPlotFileName()
filename = self.compute_name(k_filename, output_dir, self.output, id, suffix)
logger.debug("Plotting layer `{}` to `{}`".format(la, filename))
self.plot_layer(plot_ctrl, id)
plot_ctrl.ClosePlot()
if self.output and k_filename != filename:
os.replace(k_filename, filename)
if create_job:
jobfile_writer.AddGbrFile(id, os.path.basename(filename))
generated[la.layer] = os.path.basename(filename)
# Create the job file
if create_job:
jobfile_writer.CreateJobFile(self.expand_filename(output_dir, po.gerber_job_file, 'job', 'gbrjob'))
# Custom reports
regex_fname = re.compile(r'\$\{filename\(.*\)\}')
for report in self.custom_reports:
filename = report.output
content = report.content
# Replace special white spaces
content = content.replace('\\r', chr(13))
content = content.replace('\\n', chr(10))
content = content.replace('\\t', chr(9))
# Replace file names, compatible with gerber_zipper_action
content = content.replace('${basename}', GS.pcb_basename)
for name, file in generated.items():
content = content.replace('${filename('+name+')}', file)
# Replace unused layers
content = regex_fname.sub('', content)
# Create the report
logger.debug('Creating custom report `'+filename+'`')
with open(os.path.join(output_dir, filename), 'wt') as f:
f.write(content)
# Restore the eliminated layers
if exclude:
self.unfilter_pcb_components()
# Restore the list of visible layers
GS.board.SetVisibleLayers(old_visible)
self._generated_files = generated
def solve_extension(self, layer):
if self._plot_format == PLOT_FORMAT_GERBER and self.use_protel_extensions:
return layer._protel_extension
return FORMAT_EXTENSIONS[self._plot_format]
def get_targets(self, output_dir, layers):
targets = []
layers = Layer.solve(layers)
for la in layers:
id = la.id
if not GS.board.IsLayerEnabled(id):
continue
k_filename = self.expand_filename(output_dir, '%f-%i.%x', la.suffix, self.solve_extension(la))
filename = self.compute_name(k_filename, output_dir, self.output, id, la.suffix)
if GS.debug_level > 2:
logger.debug('Layer id {} file name {} ({})'.format(id, filename, k_filename))
targets.append(filename)
if self._plot_format == PLOT_FORMAT_GERBER and self.create_gerber_job_file:
targets.append(self.expand_filename(output_dir, self.gerber_job_file, 'job', 'gbrjob'))
for report in self.custom_reports:
targets.append(os.path.join(output_dir, report.output))
return targets
def read_vals_from_po(self, po):
# excludeedgelayer
if GS.ki7:
self.exclude_edge_layer = not po.GetPlotOnAllLayersSelection().Contains(GS.board.GetLayerID('Edge.Cuts'))
else:
self.exclude_edge_layer = po.GetExcludeEdgeLayer()
# plotframeref
self.plot_sheet_reference = po.GetPlotFrameRef()
# plotreference
self.plot_footprint_refs = po.GetPlotReference()
# plotvalue
self.plot_footprint_values = po.GetPlotValue()
# plotinvisibletext
self.force_plot_invisible_refs_vals = po.GetPlotInvisibleText()
# viasonmask
self.tent_vias = not po.GetPlotViaOnMaskLayer()
if GS.ki5:
# padsonsilk
self.exclude_pads_from_silkscreen = not po.GetPlotPadsOnSilkLayer()
else:
self.sketch_pads_on_fab_layers = po.GetSketchPadsOnFabLayers()
self.sketch_pad_line_width = po.GetSketchPadLineWidth()
# scaleselection
if self._plot_format != PLOT_FORMAT_GERBER:
sel = po.GetScaleSelection()
sel = sel if sel < 0 or sel > 4 else 4
self.scaling = (AUTO_SCALE, 1.0, 1.5, 2.0, 3.0)[sel]
class AnyLayer(BaseOutput):
def __init__(self):
super().__init__()
with document:
self.layers = Layer
""" *[list(dict)|list(string)|string] [all,selected,copper,technical,user,inners,outers]
List of PCB layers to plot """
def config(self, parent):
super().config(parent)
# We need layers
if isinstance(self.layers, type):
raise KiPlotConfigurationError("Missing `layers` list")
def get_targets(self, out_dir):
return self.options.get_targets(out_dir, self.layers)
@staticmethod
def layer2dict(la):
return {'layer': la.layer, 'suffix': la.suffix, 'description': la.description}
@staticmethod
def get_conf_examples(name, layers):
gb = {}
outs = [gb]
name_u = name.upper()
gb['name'] = 'basic_'+name
gb['comment'] = 'Individual layers in '+name_u+' format'
gb['type'] = name
gb['dir'] = os.path.join('Individual_Layers', name_u)
gb['layers'] = [AnyLayer.layer2dict(la) for la in layers]
return outs
def run(self, output_dir):
self.options.run(output_dir, self.layers)