diff --git a/README.md b/README.md index 69c68a10..89d8a8dc 100644 --- a/README.md +++ b/README.md @@ -3590,7 +3590,8 @@ Notes: - *frame_width*: Alias for framewidth. - `frameclearance`: [number=0] Clearance for the stencil register [mm]. - `framewidth`: [number=1] Register frame width. - - `include_scad`: [boolean=true] Include the generated OpenSCAD files. Note that this also includes the DXF files. + - `include_scad`: [boolean=true] Include the generated OpenSCAD files. + Note that this also includes the DXF files. - *pcb_thickness*: Alias for pcbthickness. - `pcbthickness`: [number=0] PCB thickness [mm]. If 0 we will ask KiCad. - `pre_transform`: [string|list(string)='_none'] Name of the filter to transform fields before applying other filters. diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index 5ef290b9..8c0c7f85 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -2488,7 +2488,8 @@ outputs: frameclearance: 0 # [number=1] Register frame width framewidth: 1 - # [boolean=true] Include the generated OpenSCAD files. Note that this also includes the DXF files + # [boolean=true] Include the generated OpenSCAD files. + # Note that this also includes the DXF files include_scad: true # [string='%f-%i%I%v.%x'] Filename for the output (%i='stencil_3d_top'|'stencil_3d_bottom'|'stencil_3d_edge', # %x='stl'|'scad'|'dxf'). Affected by global options diff --git a/kibot/out_any_stencil.py b/kibot/out_any_stencil.py new file mode 100644 index 00000000..f7e95ccf --- /dev/null +++ b/kibot/out_any_stencil.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2022 Salvador E. Tropea +# Copyright (c) 2022 Instituto Nacional de TecnologĂ­a Industrial +# License: GPL-3.0 +# Project: KiBot (formerly KiPlot) +import os +import shutil +import tempfile +from .error import PlotError +from .gs import GS +from .kiplot import run_command +from .out_base import VariantOptions +from .misc import W_AUTONONE +from .macros import macros, document, output_class # noqa: F401 +from . import log + +logger = log.get_logger() + + +class Stencil_Options(VariantOptions): + def __init__(self): + with document: + self.side = 'auto' + """ [top,bottom,auto,both] Which side of the PCB we want. Using `auto` will detect which + side contains solder paste """ + self.include_scad = True + """ Include the generated OpenSCAD files """ + self.cutout = '' + """ [string|list(string)] List of components to add a cutout based on the component courtyard. + This is useful when you have already pre-populated board and you want to populate more + components """ + self.pcbthickness = 0 + """ PCB thickness [mm]. If 0 we will ask KiCad """ + self.pcb_thickness = None + """ {pcbthickness} """ + super().__init__() + + def config(self, parent): + super().config(parent) + self.cutout = ','.join(self.force_list(self.cutout)) + + def move_output(self, src_dir, src_file, id, ext, replacement=None, patch=False, relative=False): + self._expand_id = id + self._expand_ext = ext + dst_name = self._parent.expand_filename(self._parent.output_dir, self.output) + src_name = os.path.join(src_dir, src_file) + if not os.path.isfile(src_name): + raise PlotError('Missing output file {}'.format(src_name)) + if patch: + # Adjust the names of the DXF files + with open(src_name, 'r') as f: + content = f.read() + for k, v in replacement.items(): + content = content.replace(k, v) + with open(dst_name, 'w') as f: + f.write(content) + else: + shutil.move(src_name, dst_name) + if replacement is not None: + if relative: + src_name = os.path.basename(src_name) + replacement[src_name] = os.path.basename(dst_name) + + def run(self, output): + cmd_kikit = self.ensure_tool('KiKit') + self.ensure_tool('OpenSCAD') + super().run(output) + # Apply variants and filters + filtered = self.filter_pcb_components(GS.board) + if self.side == 'auto': + detected_top, detected_bottom = self.detect_solder_paste(GS.board) + fname = self.save_tmp_board() if filtered else GS.pcb_file + if filtered: + self.unfilter_pcb_components(GS.board) + # Avoid running the tool if we will generate useless models + if self.side == 'auto' and not detected_top and not detected_bottom: + logger.warning(W_AUTONONE+'No solder paste detected, skipping stencil generation') + return + # If no PCB thickness indicated ask KiCad + if not self.pcbthickness: + ds = GS.board.GetDesignSettings() + self.pcbthickness = self.to_mm(ds.GetBoardThickness()) + # Create the command line + cmd = self.create_cmd(cmd_kikit) + # Create the outputs + with tempfile.TemporaryDirectory() as tmp: + cmd.append(fname) + cmd.append(tmp) + try: + run_command(cmd) + finally: + # Remove temporal variant + if filtered: + GS.remove_pcb_and_pro(fname) + # Now copy the files we want + # - Which side? + do_top = do_bottom = False + if self.side == 'top': + do_top = True + elif self.side == 'bottom': + do_bottom = True + elif self.side == 'both': + do_top = True + do_bottom = True + else: # auto + do_top = detected_top + do_bottom = detected_bottom + prj_name = os.path.splitext(os.path.basename(fname))[0] + self.move_outputs(tmp, prj_name, do_top, do_bottom) diff --git a/kibot/out_stencil_3d.py b/kibot/out_stencil_3d.py index 8ce45a0f..1c22b136 100644 --- a/kibot/out_stencil_3d.py +++ b/kibot/out_stencil_3d.py @@ -15,39 +15,20 @@ Dependencies: arch: openscad role: mandatory """ -import os -import shutil -import tempfile -from .error import PlotError from .gs import GS -from .kiplot import run_command -from .out_base import VariantOptions -from .misc import W_AUTONONE from .macros import macros, document, output_class # noqa: F401 +from .out_any_stencil import Stencil_Options from . import log logger = log.get_logger() -class Stencil_3D_Options(VariantOptions): +class Stencil_3D_Options(Stencil_Options): def __init__(self): with document: self.output = GS.def_global_output """ *Filename for the output (%i='stencil_3d_top'|'stencil_3d_bottom'|'stencil_3d_edge', %x='stl'|'scad'|'dxf') """ - self.side = 'auto' - """ [top,bottom,auto,both] Which side of the PCB we want. Using `auto` will detect which - side contains solder paste """ - self.include_scad = True - """ Include the generated OpenSCAD files. Note that this also includes the DXF files """ - self.cutout = '' - """ [string|list(string)] List of components to add a cutout based on the component courtyard. - This is useful when you have already pre-populated board and you want to populate more - components """ - self.pcbthickness = 0 - """ PCB thickness [mm]. If 0 we will ask KiCad """ - self.pcb_thickness = None - """ {pcbthickness} """ self.thickness = 0.15 """ *Stencil thickness [mm]. Defines amount of paste dispensed """ self.framewidth = 1 @@ -63,30 +44,7 @@ class Stencil_3D_Options(VariantOptions): self.enlarge_holes = None """ {enlarge_holes} """ super().__init__() - - def config(self, parent): - super().config(parent) - self.cutout = ','.join(self.force_list(self.cutout)) - - def move_output(self, src_dir, src_file, id, ext, replacement=None, patch=False): - self._expand_id = id - self._expand_ext = ext - dst_name = self._parent.expand_filename(self._parent.output_dir, self.output) - src_name = os.path.join(src_dir, src_file) - if not os.path.isfile(src_name): - raise PlotError('Missing output file {}'.format(src_name)) - if patch: - # Adjust the names of the DXF files - with open(src_name, 'r') as f: - content = f.read() - for k, v in replacement.items(): - content = content.replace(k, v) - with open(dst_name, 'w') as f: - f.write(content) - else: - shutil.move(src_name, dst_name) - if replacement is not None: - replacement[src_name] = os.path.basename(dst_name) + self.add_to_doc('include_scad', 'Note that this also includes the DXF files') def get_targets(self, out_dir): # TODO: auto side is tricky, needs variants applied @@ -103,6 +61,7 @@ class Stencil_3D_Options(VariantOptions): cmd.extend(['--frameclearance', str(self.frameclearance)]) if self.enlargeholes: cmd.extend(['--enlargeholes', str(self.enlargeholes)]) + return cmd def move_outputs(self, tmp, prj_name, do_top, do_bottom): replacements = {} @@ -122,53 +81,6 @@ class Stencil_3D_Options(VariantOptions): self.move_output(tmp, prj_name+'-PasteBottom.dxf', 'Stencil_For_Jig_bottom', 'dxf', replacements) self.move_output(tmp, 'bottomStencil.scad', 'Stencil_For_Jig_bottom', 'scad', replacements, patch=True) - def run(self, output): - cmd_kikit = self.ensure_tool('KiKit') - self.ensure_tool('OpenSCAD') - super().run(output) - # Apply variants and filters - filtered = self.filter_pcb_components(GS.board) - if self.side == 'auto': - detected_top, detected_bottom = self.detect_solder_paste(GS.board) - fname = self.save_tmp_board() if filtered else GS.pcb_file - if filtered: - self.unfilter_pcb_components(GS.board) - # Avoid running the tool if we will generate useless models - if self.side == 'auto' and not detected_top and not detected_bottom: - logger.warning(W_AUTONONE+'No solder paste detected, skipping stencil generation') - return - # If no PCB thickness indicated ask KiCad - if not self.pcbthickness: - ds = GS.board.GetDesignSettings() - self.pcbthickness = self.to_mm(ds.GetBoardThickness()) - # Create the command line - cmd = self.create_cmd(cmd_kikit) - # Create the outputs - with tempfile.TemporaryDirectory() as tmp: - cmd.append(fname) - cmd.append(tmp) - try: - run_command(cmd) - finally: - # Remove temporal variant - if filtered: - GS.remove_pcb_and_pro(fname) - # Now copy the files we want - # - Which side? - do_top = do_bottom = False - if self.side == 'top': - do_top = True - elif self.side == 'bottom': - do_bottom = True - elif self.side == 'both': - do_top = True - do_bottom = True - else: # auto - do_top = detected_top - do_bottom = detected_bottom - prj_name = os.path.splitext(os.path.basename(fname))[0] - self.move_outputs(tmp, prj_name, do_top, do_bottom) - @output_class class Stencil_3D(BaseOutput): # noqa: F821 diff --git a/kibot/out_stencil_for_jig.py b/kibot/out_stencil_for_jig.py index bdbc17f5..99d8edc4 100644 --- a/kibot/out_stencil_for_jig.py +++ b/kibot/out_stencil_for_jig.py @@ -15,39 +15,20 @@ Dependencies: arch: openscad role: mandatory """ -import os -import shutil -import tempfile -from .error import PlotError from .gs import GS -from .kiplot import run_command -from .out_base import VariantOptions -from .misc import W_AUTONONE from .macros import macros, document, output_class # noqa: F401 +from .out_any_stencil import Stencil_Options from . import log logger = log.get_logger() -class Stencil_For_Jig_Options(VariantOptions): +class Stencil_For_Jig_Options(Stencil_Options): def __init__(self): with document: self.output = GS.def_global_output """ *Filename for the output (%i='stencil_for_jig_top'|'stencil_for_jig_bottom', %x='stl'|'scad'|'gbp'|'gtp'|'gbrjob') """ - self.side = 'auto' - """ [top,bottom,auto,both] Which side of the PCB we want. Using `auto` will detect which - side contains solder paste """ - self.include_scad = True - """ Include the generated OpenSCAD files """ - self.cutout = '' - """ [string|list(string)] List of components to add a cutout based on the component courtyard. - This is useful when you have already pre-populated board and you want to populate more - components """ - self.pcbthickness = 0 - """ PCB thickness [mm]. If 0 we will ask KiCad """ - self.pcb_thickness = None - """ {pcbthickness} """ self.jigthickness = 3 """ *Jig thickness [mm] """ self.jig_thickness = None @@ -72,32 +53,6 @@ class Stencil_For_Jig_Options(VariantOptions): """ {jigheight} """ super().__init__() - def config(self, parent): - super().config(parent) - self.cutout = ','.join(self.force_list(self.cutout)) - - def move_output(self, src_dir, src_file, id, ext, replacement=None, patch=False, relative=False): - self._expand_id = id - self._expand_ext = ext - dst_name = self._parent.expand_filename(self._parent.output_dir, self.output) - src_name = os.path.join(src_dir, src_file) - if not os.path.isfile(src_name): - raise PlotError('Missing output file {}'.format(src_name)) - if patch: - # Adjust the names of the DXF files - with open(src_name, 'r') as f: - content = f.read() - for k, v in replacement.items(): - content = content.replace(k, v) - with open(dst_name, 'w') as f: - f.write(content) - else: - shutil.move(src_name, dst_name) - if replacement is not None: - if relative: - src_name = os.path.basename(src_name) - replacement[src_name] = os.path.basename(dst_name) - def get_targets(self, out_dir): # TODO: auto side is tricky, needs variants applied return [self._parent.expand_filename(out_dir, self.output)] @@ -131,53 +86,6 @@ class Stencil_For_Jig_Options(VariantOptions): if do_top and do_bottom: self.move_output(tmp, 'gerber/stencil.gbrjob', 'stencil_for_jig', 'gbrjob', replacements, patch=True) - def run(self, output): - cmd_kikit = self.ensure_tool('KiKit') - self.ensure_tool('OpenSCAD') - super().run(output) - # Apply variants and filters - filtered = self.filter_pcb_components(GS.board) - if self.side == 'auto': - detected_top, detected_bottom = self.detect_solder_paste(GS.board) - fname = self.save_tmp_board() if filtered else GS.pcb_file - if filtered: - self.unfilter_pcb_components(GS.board) - # Avoid running the tool if we will generate useless models - if self.side == 'auto' and not detected_top and not detected_bottom: - logger.warning(W_AUTONONE+'No solder paste detected, skipping stencil generation') - return - # If no PCB thickness indicated ask KiCad - if not self.pcbthickness: - ds = GS.board.GetDesignSettings() - self.pcbthickness = self.to_mm(ds.GetBoardThickness()) - # Create the command line - cmd = self.create_cmd(cmd_kikit) - # Create the outputs - with tempfile.TemporaryDirectory() as tmp: - cmd.append(fname) - cmd.append(tmp) - try: - run_command(cmd) - finally: - # Remove temporal variant - if filtered: - GS.remove_pcb_and_pro(fname) - # Now copy the files we want - # - Which side? - do_top = do_bottom = False - if self.side == 'top': - do_top = True - elif self.side == 'bottom': - do_bottom = True - elif self.side == 'both': - do_top = True - do_bottom = True - else: # auto - do_top = detected_top - do_bottom = detected_bottom - prj_name = os.path.splitext(os.path.basename(fname))[0] - self.move_outputs(tmp, prj_name, do_top, do_bottom) - @output_class class Stencil_For_Jig(BaseOutput): # noqa: F821