diff --git a/MANIFEST.in b/MANIFEST.in
index b996a066..38a34bdf 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -13,3 +13,4 @@ include kibot/resources/images/*.ico
include kibot/resources/pcbdraw/styles/*.json
include kibot/resources/pcbdraw/templates/*.handlebars
recursive-include kibot/resources/pcbdraw/footprints/ *.svg LICENCE *.md *.py *.pl *.png
+recursive-include kibot/resources/pcbdraw/present/ *.css *.html *.json *.js
diff --git a/README.md b/README.md
index 180c88f0..aca2fe0d 100644
--- a/README.md
+++ b/README.md
@@ -172,6 +172,9 @@ Notes:
[**KiCad PCB/SCH Diff**](https://github.com/INTI-CMNB/KiDiff) v2.4.3 [](https://github.com/INTI-CMNB/KiDiff) 
- Mandatory for `diff`
+[**markdown2**](https://pypi.org/project/markdown2/) [](https://pypi.org/project/markdown2/) [](https://packages.debian.org/bullseye/python3-markdown2)
+- Mandatory for `kikit_present`
+
[**mistune**](https://pypi.org/project/mistune/) [](https://pypi.org/project/mistune/) [](https://packages.debian.org/bullseye/python3-mistune)
- Mandatory for `populate`
@@ -2408,6 +2411,80 @@ Notes:
Internally we use 10 for low priority, 90 for high priority and 50 for most outputs.
- `run_by_default`: [boolean=true] When enabled this output will be created when no specific outputs are requested.
+* KiKit's Present - Project Presentation
+ * Type: `kikit_present`
+ * Description: Creates an HTML file showing your project.
+ It can contain one or more PCBs, showing their top and bottom sides.
+ Also includes a download link and the gerbers.
+ * Valid keys:
+ - **`comment`**: [string=''] A comment for documentation purposes.
+ - **`dir`**: [string='./'] Output directory for the generated files.
+ If it starts with `+` the rest is concatenated to the default dir.
+ - **`name`**: [string=''] Used to identify this particular output definition.
+ - **`options`**: [dict] Options for the `kikit_present` output.
+ * Valid keys:
+ - **`description`**: [string=''] Name for a markdown file containing the main part of the page to be generated.
+ This is mandatory and is the description of your project.
+ - `boards`: [dict|list(dict)] One or more boards that compose your project.
+ When empty we will use only the main PCB for the current project.
+ * Valid keys:
+ - **`back_image`**: [string=''] local*: the name of an output that renders the back.
+ If empty we use the first renderer for the back.
+ *file*: the name of the rendered image.
+ *external*: ignored, we `extrenal_config`.
+ - **`front_image`**: [string=''] local*: the name of an output that renders the front.
+ If empty we use the first renderer for the front.
+ *file*: the name of the rendered image.
+ *external*: ignored, we `extrenal_config`.
+ - **`gerbers`**: [string=''] local*: the name of a compress output.
+ If empty we use the first compress output.
+ *file*: the name of a compressed archive.
+ *external*: ignored, we `extrenal_config`.
+ - **`name`**: [string=''] Name for this board. If empty we use the name of the PCB.
+ Applies to all modes.
+ - `comment`: [string=''] A comment or description for this board.
+ Applies to all modes.
+ - `external_config`: [string=''] Name of an external KiBot configuration.
+ Only used in the *external* mode.
+ - `mode`: [string='auto'] [local,file,external] How images and gerbers are obtained.
+ *local*: Only applies to the currently selected PCB.
+ You must provide the names of the outputs used to render
+ the images and compress the gerbers.
+ When empty KiBot will use the first render/compress output
+ it finds.
+ To apply variants use `pcb_from_output` and a `pcb_variant`
+ output.
+ *file*: You must specify the file names used for the images and
+ the gerbers.
+ *external*: You must specify an external KiBot configuration.
+ It will be applied to the selected PCB to create the images and
+ the gerbers. The front image must be generated in a dir called
+ *front*, the back image in a dir called *back* and the gerbers
+ in a dir called *gerbers*.
+ - `pcb_file`: [string=''] Name of the KiCad PCB file. When empty we use the current PCB.
+ Is ignored for the *local* mode.
+ - `pcb_from_output`: [string=''] Use the PCB generated by another output.
+ Is ignored for the *file* mode.
+ - `name`: [string=''] Name of the project. Will be passed to the template.
+ If empty we use the name of the KiCad project.
+ The default template uses it for things like the page title.
+ - `repository`: [string=''] URL of the repository. Will be passed to the template.
+ - `resources`: [string|list(string)=''] A list of file name patterns for additional resources to be included.
+ I.e. images referenced in description.
+ They will be copied relative to the output dir.
+ - `template`: [string='default'] Path to a template directory or a name of built-in one.
+ See KiKit's doc/present.md for template specification.
+ - `category`: [string|list(string)=''] The category for this output. If not specified an internally defined category is used.
+ Categories looks like file system paths, i.e. PCB/fabrication/gerber.
+ - `disable_run_by_default`: [string|boolean] Use it to disable the `run_by_default` status of other output.
+ Useful when this output extends another and you don't want to generate the original.
+ Use the boolean true value to disable the output you are extending.
+ - `extends`: [string=''] Copy the `options` section from the indicated output.
+ - `output_id`: [string=''] Text to use for the %I expansion content. To differentiate variations of this output.
+ - `priority`: [number=50] [0,100] Priority for this output. High priority outputs are created first.
+ Internally we use 10 for low priority, 90 for high priority and 50 for most outputs.
+ - `run_by_default`: [boolean=true] When enabled this output will be created when no specific outputs are requested.
+
* Navigate Results
* Type: `navigate_results`
* Description: Generates a web page to navigate the generated outputs
diff --git a/debian/control b/debian/control
index 0098c939..7e85acf6 100644
--- a/debian/control
+++ b/debian/control
@@ -11,7 +11,7 @@ Package: kibot
Architecture: all
Multi-Arch: foreign
Depends: ${misc:Depends}, ${python3:Depends}, python3-yaml, kicad (>= 5.1.6), python3-wxgtk4.0, python3-xvfb
-Recommends: kibom.inti-cmnb (>= 1.8.0), kicost (>= 1.1.8), interactivehtmlbom.inti-cmnb (>= 2.4.1), imagemagick, librsvg2-bin, python3-xlsxwriter, rar, ghostscript, python3-lxml, python3-mistune, kikit
+Recommends: kibom.inti-cmnb (>= 1.8.0), kicost (>= 1.1.8), interactivehtmlbom.inti-cmnb (>= 2.4.1), imagemagick, librsvg2-bin, python3-xlsxwriter, rar, ghostscript, python3-lxml, python3-mistune, kikit, python3-markdown2
Suggests: pandoc, texlive-latex-base, texlive-latex-recommended, git, poppler-utils, kidiff, python3-numpy
Description: KiCad Bot
KiBot is a program which helps you to automate the generation of KiCad
diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml
index 4589978c..da64be74 100644
--- a/docs/samples/generic_plot.kibot.yaml
+++ b/docs/samples/generic_plot.kibot.yaml
@@ -1182,6 +1182,79 @@ outputs:
# [string=''] Board variant to apply.
# Don't use the `kicost_variant` when using internal variants/filters
variant: ''
+ # KiKit's Present - Project Presentation:
+ # It can contain one or more PCBs, showing their top and bottom sides.
+ # Also includes a download link and the gerbers.
+ - name: 'kikit_present_example'
+ comment: 'Creates an HTML file showing your project.'
+ type: 'kikit_present'
+ dir: 'Example/kikit_present_dir'
+ options:
+ # [dict|list(dict)] One or more boards that compose your project.
+ # When empty we will use only the main PCB for the current project
+ boards:
+ # [string=''] local*: the name of an output that renders the back.
+ # If empty we use the first renderer for the back.
+ # *file*: the name of the rendered image.
+ # *external*: ignored, we `extrenal_config`
+ - back_image: ''
+ # [string=''] A comment or description for this board.
+ # Applies to all modes
+ comment: ''
+ # [string=''] Name of an external KiBot configuration.
+ # Only used in the *external* mode
+ external_config: ''
+ # [string=''] local*: the name of an output that renders the front.
+ # If empty we use the first renderer for the front.
+ # *file*: the name of the rendered image.
+ # *external*: ignored, we `extrenal_config`
+ front_image: ''
+ # [string=''] local*: the name of a compress output.
+ # If empty we use the first compress output.
+ # *file*: the name of a compressed archive.
+ # *external*: ignored, we `extrenal_config`
+ gerbers: ''
+ # [string='auto'] [local,file,external] How images and gerbers are obtained.
+ # *local*: Only applies to the currently selected PCB.
+ # You must provide the names of the outputs used to render
+ # the images and compress the gerbers.
+ # When empty KiBot will use the first render/compress output
+ # it finds.
+ # To apply variants use `pcb_from_output` and a `pcb_variant`
+ # output.
+ # *file*: You must specify the file names used for the images and
+ # the gerbers.
+ # *external*: You must specify an external KiBot configuration.
+ # It will be applied to the selected PCB to create the images and
+ # the gerbers. The front image must be generated in a dir called
+ # *front*, the back image in a dir called *back* and the gerbers
+ # in a dir called *gerbers*
+ mode: 'auto'
+ # [string=''] Name for this board. If empty we use the name of the PCB.
+ # Applies to all modes
+ name: ''
+ # [string=''] Name of the KiCad PCB file. When empty we use the current PCB.
+ # Is ignored for the *local* mode
+ pcb_file: ''
+ # [string=''] Use the PCB generated by another output.
+ # Is ignored for the *file* mode
+ pcb_from_output: ''
+ # [string=''] Name for a markdown file containing the main part of the page to be generated.
+ # This is mandatory and is the description of your project
+ description: ''
+ # [string=''] Name of the project. Will be passed to the template.
+ # If empty we use the name of the KiCad project.
+ # The default template uses it for things like the page title
+ name: ''
+ # [string=''] URL of the repository. Will be passed to the template
+ repository: ''
+ # [string|list(string)=''] A list of file name patterns for additional resources to be included.
+ # I.e. images referenced in description.
+ # They will be copied relative to the output dir
+ resources: ''
+ # [string='default'] Path to a template directory or a name of built-in one.
+ # See KiKit's doc/present.md for template specification
+ template: 'default'
# Navigate Results:
- name: 'navigate_results_example'
comment: 'Generates a web page to navigate the generated outputs'
diff --git a/kibot/PcbDraw/README.md b/kibot/PcbDraw/README.md
index b0d54ebe..ba724000 100644
--- a/kibot/PcbDraw/README.md
+++ b/kibot/PcbDraw/README.md
@@ -201,3 +201,7 @@ This file comes from KiKit, but it has too much in common with `populate.py`.
- Removed the try in boardpage, too broad, doesn't help
- Removed `kikit` import, _renderBoards must be implemented in a different way
- Imported local pybars
+- Added source_front, source_back and source_gerbers to the boards.
+ So now the images and gerbers come from outside.
+- Adapted Template._renderBoards to just copy the files (keeping the extensions)
+
diff --git a/kibot/PcbDraw/present.py b/kibot/PcbDraw/present.py
index 6fbd53b9..82c30d5c 100644
--- a/kibot/PcbDraw/present.py
+++ b/kibot/PcbDraw/present.py
@@ -26,7 +26,7 @@ def resolveTemplatePath(path):
TEMPLATES = os.path.join(PKG_BASE, "resources/present/templates")
if os.path.exists(os.path.join(TEMPLATES, path, "template.json")):
return os.path.join(TEMPLATES, path)
- raise RuntimeError("'{}' is not a name or a path for existing template. Perhaps you miss template.json in the template?")
+ raise RuntimeError("'{}' is not a name or a path for existing template. Perhaps you miss template.json in the template?".format(path))
def readTemplate(path):
"""
@@ -85,14 +85,17 @@ class Template:
"""
self.extraResources.append(resource)
- def addBoard(self, name, comment, boardfile):
+ def addBoard(self, name, comment, boardfile, front, back, gerbers):
"""
Add board
"""
self.boards.append({
"name": name,
"comment": comment,
- "source": boardfile
+ "source": boardfile,
+ "source_front": front,
+ "source_back": back,
+ "source_gerbers": gerbers
})
def _renderBoards(self, outputDirectory):
@@ -100,31 +103,19 @@ class Template:
Convert all boards to images and gerber exports. Enrich self.boards
with paths of generated files
"""
- pcbdraw = shutil.which("pcbdraw")
- if not pcbdraw:
- raise RuntimeError("PcbDraw needs to be installed in order to render boards")
-
dirPrefix = "boards"
boardDir = os.path.join(outputDirectory, dirPrefix)
Path(boardDir).mkdir(parents=True, exist_ok=True)
for boardDesc in self.boards:
boardName = os.path.basename(boardDesc["source"]).replace(".kicad_pcb", "")
- boardDesc["front"] = os.path.join(dirPrefix, boardName + "-front.png")
- boardDesc["back"] = os.path.join(dirPrefix, boardName + "-back.png")
- boardDesc["gerbers"] = os.path.join(dirPrefix, boardName + "-gerbers.zip")
+ boardDesc["front"] = os.path.join(dirPrefix, boardName + "-front"+os.path.splitext(boardDesc["source_front"])[1])
+ boardDesc["back"] = os.path.join(dirPrefix, boardName + "-back"+os.path.splitext(boardDesc["source_back"])[1])
+ boardDesc["gerbers"] = os.path.join(dirPrefix, boardName + "-gerbers"+os.path.splitext(boardDesc["source_gerbers"])[1])
boardDesc["file"] = os.path.join(dirPrefix, boardName + ".kicad_pcb")
-
- subprocess.check_call([pcbdraw, "plot", "--vcuts=Cmts.User", "--silent", "--side=front", boardDesc["source"],
- os.path.join(outputDirectory, boardDesc["front"])])
- subprocess.check_call([pcbdraw, "plot", "--vcuts=Cmts.User", "--silent", "--side=back", boardDesc["source"],
- os.path.join(outputDirectory, boardDesc["back"])])
-
- tmp = tempfile.mkdtemp()
- export.gerberImpl(boardDesc["source"], tmp)
- shutil.make_archive(os.path.join(outputDirectory, boardDesc["gerbers"])[:-4], "zip", tmp)
- shutil.rmtree(tmp)
-
shutil.copy(boardDesc["source"], os.path.join(outputDirectory, boardDesc["file"]))
+ shutil.copy(boardDesc["source_front"], os.path.join(outputDirectory, boardDesc["front"]))
+ shutil.copy(boardDesc["source_back"], os.path.join(outputDirectory, boardDesc["back"]))
+ shutil.copy(boardDesc["source_gerbers"], os.path.join(outputDirectory, boardDesc["gerbers"]))
def render(self, outputDirectory):
self._copyResources(outputDirectory)
@@ -183,6 +174,6 @@ def boardpage(outdir, description, board, resource, template, repository, name):
template.setName(name)
for r in resource:
template.addResource(r)
- for name, comment, file in board:
- template.addBoard(name, comment, file)
+ for name, comment, file, front, back, gerbers in board:
+ template.addBoard(name, comment, file, front, back, gerbers)
template.render(outdir)
diff --git a/kibot/misc.py b/kibot/misc.py
index 7140fe3f..b850debf 100644
--- a/kibot/misc.py
+++ b/kibot/misc.py
@@ -265,6 +265,8 @@ KICAD5_SVG_SCALE = 116930/297002200
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0'
# Text used to disable 3D models
DISABLE_3D_MODEL_TEXT = '_Disabled_by_KiBot'
+RENDERERS = ['pcbdraw', 'render_3d']
+PCB_GENERATORS = ['pcb_variant', 'panelize']
class Rect(object):
diff --git a/kibot/optionable.py b/kibot/optionable.py
index 3a6ade84..d3cf2ff8 100644
--- a/kibot/optionable.py
+++ b/kibot/optionable.py
@@ -436,6 +436,38 @@ class Optionable(object):
def color_str_to_rgb(self, color):
return self.color_to_rgb(self.parse_one_color(color))
+ def save_renderer_options(self):
+ """ Save the current renderer settings """
+ options = self._renderer.options
+ self.old_filters_to_expand = options._filters_to_expand
+ self.old_show_components = options.show_components
+ self.old_highlight = options.highlight
+ self.old_output = options.output
+ self.old_dir = self._renderer.dir
+ self.old_done = self._renderer._done
+ if self._renderer_is_pcbdraw:
+ self.old_bottom = options.bottom
+ self.old_add_to_variant = options.add_to_variant
+ else: # render_3D
+ self.old_view = options.view
+ self.old_show_all_components = options._show_all_components
+
+ def restore_renderer_options(self):
+ """ Restore the renderer settings """
+ options = self._renderer.options
+ options._filters_to_expand = self.old_filters_to_expand
+ options.show_components = self.old_show_components
+ options.highlight = self.old_highlight
+ options.output = self.old_output
+ self._renderer.dir = self.old_dir
+ self._renderer._done = self.old_done
+ if self._renderer_is_pcbdraw:
+ options.bottom = self.old_bottom
+ options.add_to_variant = self.old_add_to_variant
+ else: # render_3D
+ options.view = self.old_view
+ options._show_all_components = self.old_show_all_components
+
class BaseOptions(Optionable):
""" A class to validate and hold output options.
diff --git a/kibot/out_base.py b/kibot/out_base.py
index a34e2479..c16d4acc 100644
--- a/kibot/out_base.py
+++ b/kibot/out_base.py
@@ -118,6 +118,9 @@ class BaseOutput(RegOutput):
return [GS.sch_file]
return [GS.pcb_file]
+ def get_extension(self):
+ return self.options._expand_ext
+
def config(self, parent):
if self._tree and not self._configured and isinstance(self.extends, str) and self.extends:
logger.debug("Extending `{}` from `{}`".format(self.name, self.extends))
diff --git a/kibot/out_kikit_present.py b/kibot/out_kikit_present.py
new file mode 100644
index 00000000..61613321
--- /dev/null
+++ b/kibot/out_kikit_present.py
@@ -0,0 +1,344 @@
+# -*- 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)
+"""
+Dependencies:
+ - name: markdown2
+ python_module: true
+ debian: python3-markdown2
+ arch: python-markdown2
+ role: mandatory
+"""
+import os
+from tempfile import NamedTemporaryFile, TemporaryDirectory
+from .error import KiPlotConfigurationError
+from .misc import W_PCBDRAW, RENDERERS, PCB_GENERATORS
+from .gs import GS
+from .kiplot import config_output, run_output, get_output_dir, load_board
+from .optionable import BaseOptions, Optionable
+from .registrable import RegOutput
+from .PcbDraw.present import boardpage
+from .macros import macros, document, output_class # noqa: F401
+from . import log
+
+
+logger = log.get_logger()
+
+
+def pcbdraw_warnings(tag, msg):
+ logger.warning('{}({}) {}'.format(W_PCBDRAW, tag, msg))
+
+
+def _get_tmp_name(ext):
+ with NamedTemporaryFile(mode='w', suffix='.'+ext, delete=False) as f:
+ f.close()
+ return f.name
+
+
+class PresentBoards(Optionable):
+ def __init__(self):
+ super().__init__()
+ with document:
+ self.mode = 'auto'
+ """ [local,file,external] How images and gerbers are obtained.
+ *local*: Only applies to the currently selected PCB.
+ You must provide the names of the outputs used to render
+ the images and compress the gerbers.
+ When empty KiBot will use the first render/compress output
+ it finds.
+ To apply variants use `pcb_from_output` and a `pcb_variant`
+ output.
+ *file*: You must specify the file names used for the images and
+ the gerbers.
+ *external*: You must specify an external KiBot configuration.
+ It will be applied to the selected PCB to create the images and
+ the gerbers. The front image must be generated in a dir called
+ *front*, the back image in a dir called *back* and the gerbers
+ in a dir called *gerbers* """
+ self.name = ''
+ """ *Name for this board. If empty we use the name of the PCB.
+ Applies to all modes """
+ self.comment = ''
+ """ A comment or description for this board.
+ Applies to all modes """
+ self.pcb_file = ''
+ """ Name of the KiCad PCB file. When empty we use the current PCB.
+ Is ignored for the *local* mode """
+ self.pcb_from_output = ''
+ """ Use the PCB generated by another output.
+ Is ignored for the *file* mode """
+ self.front_image = ''
+ """ *local*: the name of an output that renders the front.
+ If empty we use the first renderer for the front.
+ *file*: the name of the rendered image.
+ *external*: ignored, we `extrenal_config` """
+ self.back_image = ''
+ """ *local*: the name of an output that renders the back.
+ If empty we use the first renderer for the back.
+ *file*: the name of the rendered image.
+ *external*: ignored, we `extrenal_config` """
+ self.gerbers = ''
+ """ *local*: the name of a compress output.
+ If empty we use the first compress output.
+ *file*: the name of a compressed archive.
+ *external*: ignored, we `extrenal_config` """
+ self.external_config = ''
+ """ Name of an external KiBot configuration.
+ Only used in the *external* mode """
+
+ def config(self, parent):
+ super().config(parent)
+ if not self.name:
+ self.name = GS.pcb_basename
+ if not self.pcb_file:
+ self.pcb_file = GS.pcb_file
+ if self.mode == 'file':
+ files = [self.front_image, self.back_image, self.gerbers]
+ for f in files:
+ if not os.path.isfile(f):
+ raise KiPlotConfigurationError('Missing file: `{}`'.format(f))
+ elif self.mode == 'external':
+ if not self.external_config:
+ raise KiPlotConfigurationError('`external_config` must be specified for the external mode')
+ if not os.path.isfile(self.external_config):
+ raise KiPlotConfigurationError('Missing external config: `{}`'.format(self.external_config))
+ if self.pcb_from_output and self.mode != 'file':
+ out = RegOutput.get_output(self.pcb_from_output)
+ self._pcb_from_output = out
+ if out is None:
+ raise KiPlotConfigurationError('Unknown output `{}` selected in board {}'.
+ format(self.pcb_from_output, self.name))
+
+ def solve_file(self):
+ return self.name, self.comment, self.pcb_file, self.front_image, self.back_image, self.gerbers
+
+ def generate_image(self, back, tmp_name):
+ self.save_renderer_options()
+ options = self._renderer.options
+ logger.debug('Starting renderer with back: {}, name: {}'.format(back, tmp_name))
+ # Configure it according to our needs
+ options._filters_to_expand = False
+ options.show_components = None if self._renderer_is_pcbdraw else []
+ options.highlight = []
+ options.output = tmp_name
+ self._renderer._done = False
+ if self._renderer_is_pcbdraw:
+ options.add_to_variant = False
+ options.bottom = back
+ else: # render_3D
+ options.view = 'Z' if back else 'z'
+ options._show_all_components = False
+ run_output(self._renderer)
+ self.restore_renderer_options()
+
+ def do_compress(self, tmp_name, out):
+ tree = {'name': '_temporal_compress_gerbers',
+ 'type': 'compress',
+ 'comment': 'Internally created to compress gerbers',
+ 'options': {'output': tmp_name, 'files': [{'from_output': out.name, 'dest': '/'}]}}
+ out = RegOutput.get_class_for('compress')()
+ out.set_tree(tree)
+ config_output(out)
+ logger.debug('Creating gerbers archive ...')
+ out.options.run(tmp_name)
+
+ def generate_archive(self, out, tmp_name):
+ out.options
+ logger.debug('Starting gerber name: {}'.format(out.name))
+ # Save options
+ old_dir = out.dir
+ old_done = out._done
+ old_variant = out.options.variant
+ # Configure it according to our needs
+ with TemporaryDirectory() as tmp_dir:
+ logger.debug('Generating the gerbers at '+tmp_dir)
+ out.done = False
+ out.dir = tmp_dir
+ out.options.variant = None
+ run_output(out)
+ self.do_compress(tmp_name, out)
+ # Restore options
+ out.dir = old_dir
+ out._done = old_done
+ out.options.variant = old_variant
+
+ def solve_pcb(self):
+ if not self.pcb_from_output:
+ return False
+ out_name = self.pcb_from_output
+ out = RegOutput.get_output(out_name)
+ if out is None:
+ raise KiPlotConfigurationError('Unknown output `{}` selected in board {}'. format(out_name, self.name))
+ if out.type not in PCB_GENERATORS:
+ raise KiPlotConfigurationError("Output `{}` can't be used to render the PCB, must be {}".
+ format(out, PCB_GENERATORS))
+ config_output(out)
+ run_output(out)
+ new_pcb = out.get_targets(get_output_dir(out.dir, out))[0]
+ GS.board = None
+ self.old_pcb = GS.pcb_file
+ GS.set_pcb(new_pcb)
+ load_board()
+ self.new_pcb = new_pcb
+ return True
+
+ def solve_local_image(self, out_name, back=False):
+ if not out_name:
+ out = next(filter(lambda x: x.type in RENDERERS, RegOutput.get_outputs()), None)
+ if not out:
+ raise KiPlotConfigurationError('No renderer output found, must be {}'.format(RENDERERS))
+ else:
+ out = RegOutput.get_output(out_name)
+ if out is None:
+ raise KiPlotConfigurationError('Unknown output `{}` selected in board {}'. format(out_name, self.name))
+ if out.type not in RENDERERS:
+ raise KiPlotConfigurationError("Output `{}` can't be used to render the PCB, must be {}".
+ format(out, RENDERERS))
+ config_output(out)
+ self._renderer = out
+ self._renderer_is_pcbdraw = out.type == 'pcbdraw'
+ tmp_name = _get_tmp_name(out.get_extension())
+ self.temporals.append(tmp_name)
+ self.generate_image(back, tmp_name)
+ return tmp_name
+
+ def solve_local_gerbers(self, out_name):
+ if not out_name:
+ out = next(filter(lambda x: x.type == 'gerber', RegOutput.get_outputs()), None)
+ if not out:
+ raise KiPlotConfigurationError('No gerber output found')
+ else:
+ out = RegOutput.get_output(out_name)
+ if out is None:
+ raise KiPlotConfigurationError('Unknown output `{}` selected in board {}'. format(out_name, self.name))
+ if out.type != 'gerber':
+ raise KiPlotConfigurationError("Output `{}` must be `gerber` type, not {}". format(out, out.type))
+ config_output(out)
+ tmp_name = _get_tmp_name('zip')
+ self.temporals.append(tmp_name)
+ # Generate the archive
+ self.generate_archive(out, tmp_name)
+ return tmp_name
+
+ def solve_local(self):
+ fname = GS.pcb_file
+ pcb_changed = self.solve_pcb()
+ front_image = self.solve_local_image(self.front_image)
+ back_image = self.solve_local_image(self.back_image, back=True)
+ gerbers = self.solve_local_gerbers(self.gerbers)
+ if pcb_changed:
+ fname = self.new_pcb
+ GS.set_pcb(self.old_pcb)
+ GS.reload_project(GS.pro_file)
+ return self.name, self.comment, fname, front_image, back_image, gerbers
+
+ def solve(self, temporals):
+ self.temporals = temporals
+ if self.mode == 'file':
+ return self.solve_file()
+ elif self.mode == 'local':
+ return self.solve_local()
+
+
+class KiKit_PresentOptions(BaseOptions):
+ def __init__(self):
+ with document:
+ self.description = ''
+ """ *Name for a markdown file containing the main part of the page to be generated.
+ This is mandatory and is the description of your project """
+ self.boards = PresentBoards
+ """ [dict|list(dict)] One or more boards that compose your project.
+ When empty we will use only the main PCB for the current project """
+ self.resources = Optionable
+ """ [string|list(string)=''] A list of file name patterns for additional resources to be included.
+ I.e. images referenced in description.
+ They will be copied relative to the output dir """
+ self.template = 'default'
+ """ Path to a template directory or a name of built-in one.
+ See KiKit's doc/present.md for template specification """
+ self.repository = ''
+ """ URL of the repository. Will be passed to the template """
+ self.name = ''
+ """ Name of the project. Will be passed to the template.
+ If empty we use the name of the KiCad project.
+ The default template uses it for things like the page title """
+ super().__init__()
+
+ def config(self, parent):
+ super().config(parent)
+ # Validate the input file name
+ if not self.description:
+ raise KiPlotConfigurationError('You must specify an input markdown file using `description`')
+ if not os.path.isfile(self.description):
+ raise KiPlotConfigurationError('Missing description file `{}`'.format(self.description))
+ # List of boards
+ if isinstance(self.boards, type):
+ a_board = PresentBoards()
+ a_board.fill_empty_values()
+ self.boards = [a_board]
+ elif isinstance(self.boards, PresentBoards):
+ self.boards = [self.boards]
+ # else ... we have a list of boards
+ self.resources = self.force_list(self.resources, comma_sep=False)
+ if not self.name:
+ self.name = GS.pcb_basename
+ # Make sure the template exists
+ if not os.path.exists(os.path.join(self.template, "template.json")):
+ try:
+ self.template = GS.get_resource_path(os.path.join('pcbdraw', 'present', 'templates', self.template))
+ except SystemExit:
+ raise KiPlotConfigurationError('Missing template `{}`'.format(self.template))
+
+# def get_targets(self, out_dir):
+# img_dir = os.path.dirname(self._parent.expand_filename(out_dir, self.imgname))
+# return [self._parent.expand_filename(out_dir, self.get_out_file_name()), img_dir]
+
+ def generate_images(self, dir_name, content):
+ # Memorize the current options
+ self.save_options()
+ dir = os.path.dirname(os.path.join(dir_name, self.imgname))
+ if not os.path.exists(dir):
+ os.makedirs(dir)
+ counter = 0
+ for item in content:
+ if item["type"] != "steps":
+ continue
+ for x in item["steps"]:
+ counter += 1
+ filename = self.imgname.replace('%d', str(counter))
+ x["img"] = self.generate_image(x["side"], x["components"], x["active_components"], filename)
+ # Restore the options
+ self.restore_options()
+ return content
+
+ def run(self, dir_name):
+ # Generate missing images
+ board = []
+ temporals = []
+ try:
+ for brd in self.boards:
+ board.append(brd.solve(temporals))
+ try:
+ boardpage(dir_name, self.description, board, self.resources, self.template, self.repository, self.name)
+ except RuntimeError as e:
+ raise KiPlotConfigurationError('KiKit present error: '+str(e))
+ finally:
+ for f in temporals:
+ if os.path.isfile(f):
+ os.remove(f)
+
+
+@output_class
+class KiKit_Present(BaseOutput): # noqa: F821
+ """ KiKit's Present - Project Presentation
+ Creates an HTML file showing your project.
+ It can contain one or more PCBs, showing their top and bottom sides.
+ Also includes a download link and the gerbers. """
+ def __init__(self):
+ super().__init__()
+ with document:
+ self.options = KiKit_PresentOptions
+ """ *[dict] Options for the `kikit_present` output """
+ self._category = 'PCB/docs'
diff --git a/kibot/out_populate.py b/kibot/out_populate.py
index 84ea7bb4..661bc8b4 100644
--- a/kibot/out_populate.py
+++ b/kibot/out_populate.py
@@ -15,7 +15,7 @@ import os
from tempfile import NamedTemporaryFile
# Here we import the whole module to make monkeypatch work
from .error import KiPlotConfigurationError
-from .misc import W_PCBDRAW
+from .misc import W_PCBDRAW, RENDERERS
from .gs import GS
from .kiplot import config_output, run_output
from .optionable import Optionable
@@ -106,41 +106,9 @@ class PopulateOptions(VariantOptions):
run_output(self._renderer)
return options.expand_filename_both(name, is_sch=False)
- def save_options(self):
- """ Save the current renderer settings """
- options = self._renderer.options
- self.old_filters_to_expand = options._filters_to_expand
- self.old_show_components = options.show_components
- self.old_highlight = options.highlight
- self.old_output = options.output
- self.old_dir = self._renderer.dir
- self.old_done = self._renderer._done
- if self._renderer_is_pcbdraw:
- self.old_bottom = options.bottom
- self.old_add_to_variant = options.add_to_variant
- else: # render_3D
- self.old_view = options.view
- self.old_show_all_components = options._show_all_components
-
- def restore_options(self):
- """ Restore the renderer settings """
- options = self._renderer.options
- options._filters_to_expand = self.old_filters_to_expand
- options.show_components = self.old_show_components
- options.highlight = self.old_highlight
- options.output = self.old_output
- self._renderer.dir = self.old_dir
- self._renderer._done = self.old_done
- if self._renderer_is_pcbdraw:
- options.bottom = self.old_bottom
- options.add_to_variant = self.old_add_to_variant
- else: # render_3D
- options.view = self.old_view
- options._show_all_components = self.old_show_all_components
-
def generate_images(self, dir_name, content):
# Memorize the current options
- self.save_options()
+ self.save_renderer_options()
dir = os.path.dirname(os.path.join(dir_name, self.imgname))
if not os.path.exists(dir):
os.makedirs(dir)
@@ -153,7 +121,7 @@ class PopulateOptions(VariantOptions):
filename = self.imgname.replace('%d', str(counter))
x["img"] = self.generate_image(x["side"], x["components"], x["active_components"], filename)
# Restore the options
- self.restore_options()
+ self.restore_renderer_options()
return content
def run(self, dir_name):
@@ -169,8 +137,8 @@ class PopulateOptions(VariantOptions):
if out is None:
raise KiPlotConfigurationError('Unknown output `{}` selected in {}'.format(self.renderer, self._parent))
config_output(out)
- if out.type not in ['pcbdraw', 'render_3d']:
- raise KiPlotConfigurationError('The `renderer` must be `pcbdraw` or `render_3d` type, not {}'.format(out.type))
+ if out.type not in RENDERERS:
+ raise KiPlotConfigurationError('The `renderer` must be {} type, not {}'.format(RENDERERS, out.type))
self._renderer = out
self._renderer_is_pcbdraw = out.type == 'pcbdraw'
# Load the input content
diff --git a/kibot/resources/pcbdraw/present/templates/default/.gitignore b/kibot/resources/pcbdraw/present/templates/default/.gitignore
new file mode 100644
index 00000000..b512c09d
--- /dev/null
+++ b/kibot/resources/pcbdraw/present/templates/default/.gitignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/kibot/resources/pcbdraw/present/templates/default/css/styles.css b/kibot/resources/pcbdraw/present/templates/default/css/styles.css
new file mode 100644
index 00000000..abc0ad18
--- /dev/null
+++ b/kibot/resources/pcbdraw/present/templates/default/css/styles.css
@@ -0,0 +1 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}h1{font-size:2em;margin:.67em 0}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}b,strong{font-weight:bolder}code{font-family:monospace,monospace;font-size:1em}small{font-size:80%}img{border-style:none}input{font-family:inherit;font-size:100%;line-height:1.15;margin:0}input{overflow:visible}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}[hidden]{display:none}h1,h2,h3,h4,h5,h6,p,pre{margin:0}ul{margin:0;padding:0}ul{list-style:none}html{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}*{box-sizing:border-box;border:0 solid #e2e8f0}img{border-style:solid}input::-webkit-input-placeholder{color:#a0aec0}input::-moz-placeholder{color:#a0aec0}input:-ms-input-placeholder{color:#a0aec0}input::-ms-input-placeholder{color:#a0aec0}input::placeholder{color:#a0aec0}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}input{padding:0;line-height:inherit;color:inherit}code,pre{font-family:Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}img,object{display:block;vertical-align:middle}img{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}.bg-gray-200{background-color:#edf2f7}.rounded{border-radius:.25rem}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.hidden{display:none}.flex-wrap{flex-wrap:wrap}.flex-auto{flex:1 1 auto}.clearfix:after{content:"";display:table;clear:both}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mt-16{margin-top:4rem}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.p-2{padding:.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.top-0{top:0}.right-0{right:0}.bottom-0{bottom:0}.left-0{left:0}.text-center{text-align:center}.text-right{text-align:right}.text-sm{font-size:.875rem}.w-full{width:100%}body{font-size:1rem;font-weight:400;color:#718096}h1{font-size:1.875rem}h1,h2{font-weight:700;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;overflow-wrap:normal;word-break:normal;color:#1a202c;padding-top:1.5rem;padding-bottom:.5rem}h2{font-size:1.5rem}h3{font-weight:700;font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;overflow-wrap:normal;word-break:normal;color:#1a202c;padding-top:1.5rem;padding-bottom:.5rem;font-size:1.25rem}p{text-align:justify}a{color:#63b3ed}a:hover{color:#2b6cb0}ul{list-style-type:disc;list-style-position:inside;margin-top:.25rem;margin-bottom:.25rem}code{padding:.25rem}code,pre code{background-color:#edf2f7}pre code{display:block;margin-top:1rem;margin-bottom:1rem;padding:1rem .75rem;width:100%}img{max-width:1024px;margin:.5rem auto}.boardPreview{display:block;max-width:100%;max-height:400px;width:auto;height:auto;margin-left:auto;margin-right:auto}@media (min-width:768px){.md\:w-1\/3{width:33.333333%}}
\ No newline at end of file
diff --git a/kibot/resources/pcbdraw/present/templates/default/index.html b/kibot/resources/pcbdraw/present/templates/default/index.html
new file mode 100644
index 00000000..ad1939a8
--- /dev/null
+++ b/kibot/resources/pcbdraw/present/templates/default/index.html
@@ -0,0 +1,55 @@
+
+
+
+