From de1fc0529853e9c67700373a9485fd50cdffbe3d Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 26 Jan 2024 13:04:27 -0300 Subject: [PATCH] [KiCanvas][Added] Some very raw support Closes #572 --- CHANGELOG.md | 3 + docs/samples/generic_plot.kibot.yaml | 30 +++ docs/source/configuration/outputs.rst | 2 + .../source/configuration/outputs/kicanvas.rst | 62 ++++++ docs/source/configuration/sup_outputs.rst | 1 + kibot/gs.py | 46 ++++- kibot/out_kicanvas.py | 177 ++++++++++++++++++ tests/yaml_samples/kicanvas_1.kibot.yaml | 15 ++ 8 files changed, 331 insertions(+), 5 deletions(-) create mode 100644 docs/source/configuration/outputs/kicanvas.rst create mode 100644 kibot/out_kicanvas.py create mode 100644 tests/yaml_samples/kicanvas_1.kibot.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 90a417fa0..1a498d8f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.6.4] - UNRELEASED ### Added +- New outputs: + - KiRi: interactive diff + - KiCanvas: on-line schematic/PCB browser - General: - Operations that copies the project now also copies the PRL and the DRU - Files named *.kibot.yml are also detected as configuration files diff --git a/docs/samples/generic_plot.kibot.yaml b/docs/samples/generic_plot.kibot.yaml index f1bf26f59..1dc1e77b4 100644 --- a/docs/samples/generic_plot.kibot.yaml +++ b/docs/samples/generic_plot.kibot.yaml @@ -1517,6 +1517,36 @@ outputs: # variants with the ';' (semicolon) character. # This isn't related to the KiBot concept of variants variant: '' + # KiCanvas: + # + - name: 'kicanvas_example' + comment: 'Generates an interactive web page to browse the schematic and/or PCB.' + type: 'kicanvas' + dir: 'Example/kicanvas_dir' + options: + # [string='full'] [full,basic,none] Which controls are displayed + controls: 'full' + # [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' + # [boolean=true] Show the download button + download: true + # [boolean=true] Download the script and use a copy + local_script: true + # [boolean=true] Show the overlay asking to click + overlay: true + # [string|list(string)='_none'] Name of the filter to transform fields before applying other filters. + # A short-cut to use for simple cases where a variant is an overkill + pre_transform: '_none' + # [string|list(string)] [schematic,pcb,project] Source to display + source: 'schematic' + # [string=''] Text used to replace the sheet title. %VALUE expansions are allowed. + # If it starts with `+` the text is concatenated + title: '' + # URL for the KiCanvas script + url_script: 'https://kicanvas.org/kicanvas/kicanvas.js' + # [string=''] Board variant to apply + variant: '' # KiCost (KiCad Cost calculator): # For more information: https://github.com/INTI-CMNB/KiCost # This output is what you get from the KiCost plug-in (eeschema). diff --git a/docs/source/configuration/outputs.rst b/docs/source/configuration/outputs.rst index 46e780e3f..ac89e6e2a 100644 --- a/docs/source/configuration/outputs.rst +++ b/docs/source/configuration/outputs.rst @@ -87,6 +87,7 @@ The available values for *type* are: - ``report`` generates a report about the PDF. Can include images from the above outputs. - ``diff`` creates PDF files showing schematic or PCB changes. + - ``kiri`` creates an interactive web page showing schematic or PCB changes. .. index:: pair: supported; Bill of Materials @@ -128,6 +129,7 @@ The available values for *type* are: - ``kikit_present`` To create a project presentation web page. - ``navigate_results`` generates web pages to navigate the generated outputs. + - ``kicanvas`` creates a web page to display the schematic and/or PCB .. index:: pair: supported; fabrication helpers diff --git a/docs/source/configuration/outputs/kicanvas.rst b/docs/source/configuration/outputs/kicanvas.rst new file mode 100644 index 000000000..ab558b2b4 --- /dev/null +++ b/docs/source/configuration/outputs/kicanvas.rst @@ -0,0 +1,62 @@ +.. Automatically generated by KiBot, please don't edit this file + +.. index:: + pair: KiCanvas; kicanvas + +KiCanvas +~~~~~~~~ + +Generates an interactive web page to browse the schematic and/or PCB. + + +Type: ``kicanvas`` + +Categories: **PCB/docs**, **Schematic/docs** + +Parameters: + +- **comment** :index:`: ` [string=''] A comment for documentation purposes. It helps to identify the output. +- **dir** :index:`: ` [string='./'] Output directory for the generated files. + If it starts with `+` the rest is concatenated to the default dir. +- **name** :index:`: ` [string=''] Used to identify this particular output definition. + Avoid using `_` as first character. These names are reserved for KiBot. +- **options** :index:`: ` [dict] Options for the KiCanvas output. + + - Valid keys: + + - **local_script** :index:`: ` [boolean=true] Download the script and use a copy. + - **source** :index:`: ` [string|list(string)] [schematic,pcb,project] Source to display. + - ``controls`` :index:`: ` [string='full'] [full,basic,none] Which controls are displayed. + - ``dnf_filter`` :index:`: ` [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. + + - ``download`` :index:`: ` [boolean=true] Show the download button. + - ``overlay`` :index:`: ` [boolean=true] Show the overlay asking to click. + - ``pre_transform`` :index:`: ` [string|list(string)='_none'] Name of the filter to transform fields before applying other filters. + A short-cut to use for simple cases where a variant is an overkill. + + - ``title`` :index:`: ` [string=''] Text used to replace the sheet title. %VALUE expansions are allowed. + If it starts with `+` the text is concatenated. + - ``url_script`` :index:`: ` URL for the KiCanvas script. + - ``variant`` :index:`: ` [string=''] Board variant to apply. + +- **output** :index:`: ` [string='%f-%i%I%v.%x'] Filename for the output (%i=kicanvas, %x=html). Affected by global options. +- **type** :index:`: ` 'kicanvas' +- ``category`` :index:`: ` [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**. + The categories are currently used for `navigate_results`. + +- ``disable_run_by_default`` :index:`: ` [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`` :index:`: ` [string=''] Copy the `options` section from the indicated output. + Used to inherit options from another output of the same type. +- ``groups`` :index:`: ` [string|list(string)=''] One or more groups to add this output. In order to catch typos + we recommend to add outputs only to existing groups. You can create an empty group if + needed. + +- ``output_id`` :index:`: ` [string=''] Text to use for the %I expansion content. To differentiate variations of this output. +- ``priority`` :index:`: ` [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`` :index:`: ` [boolean=true] When enabled this output will be created when no specific outputs are requested. + diff --git a/docs/source/configuration/sup_outputs.rst b/docs/source/configuration/sup_outputs.rst index 70c571d5f..570a70210 100644 --- a/docs/source/configuration/sup_outputs.rst +++ b/docs/source/configuration/sup_outputs.rst @@ -29,6 +29,7 @@ Notes: outputs/ibom outputs/info outputs/kibom + outputs/kicanvas outputs/kicost outputs/kikit_present outputs/kiri diff --git a/kibot/gs.py b/kibot/gs.py index 29e69a8f9..c915aa32e 100644 --- a/kibot/gs.py +++ b/kibot/gs.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2020-2023 Salvador E. Tropea -# Copyright (c) 2020-2023 Instituto Nacional de Tecnología Industrial +# Copyright (c) 2020-2024 Salvador E. Tropea +# Copyright (c) 2020-2024 Instituto Nacional de Tecnología Industrial # License: GPL-3.0 # Project: KiBot (formerly KiPlot) +from contextlib import contextmanager import os import re import json @@ -52,6 +53,7 @@ class GS(object): pcb_no_ext = None # /.../dir/pcb pcb_dir = None # /.../dir pcb_basename = None # pcb + pcb_fname = None # pcb.kicad_pcb pcb_last_dir = None # dir # SCH name and useful parts sch_file = None # /.../dir/file.sch @@ -59,12 +61,14 @@ class GS(object): sch_dir = None # /.../dir sch_last_dir = None # dir sch_basename = None # file + sch_fname = None # file.kicad_sch # Project and useful parts pro_file = None # /.../dir/file.kicad_pro (or .pro) pro_no_ext = None # /.../dir/file pro_dir = None # /.../dir pro_last_dir = None # dir pro_basename = None # file + pro_fname = None # file.kicad_pro (or .pro) pro_ext = '.pro' pro_variables = None # KiCad 6 text variables defined in the project vars_regex = re.compile(r'\$\{([^\}]+)\}') @@ -199,7 +203,8 @@ def set_sch(name): if name: name = os.path.abspath(name) GS.sch_file = name - GS.sch_basename = os.path.splitext(os.path.basename(name))[0] + GS.sch_fname = os.path.basename(name) + GS.sch_basename = os.path.splitext(GS.sch_fname)[0] GS.sch_no_ext = os.path.splitext(name)[0] GS.sch_dir = os.path.dirname(name) GS.sch_last_dir = os.path.basename(GS.sch_dir) @@ -209,7 +214,8 @@ def set_pcb(name): if name: name = os.path.abspath(name) GS.pcb_file = name - GS.pcb_basename = os.path.splitext(os.path.basename(name))[0] + GS.pcb_fname = os.path.basename(name) + GS.pcb_basename = os.path.splitext(GS.pcb_fname)[0] GS.pcb_no_ext = os.path.splitext(name)[0] GS.pcb_dir = os.path.dirname(name) GS.pcb_last_dir = os.path.basename(GS.pcb_dir) @@ -219,7 +225,8 @@ def set_pro(name): if name: name = os.path.abspath(name) GS.pro_file = name - GS.pro_basename = os.path.splitext(os.path.basename(name))[0] + GS.pro_fname = os.path.basename(name) + GS.pro_basename = os.path.splitext(GS.pro_fname)[0] GS.pro_no_ext = os.path.splitext(name)[0] GS.pro_dir = os.path.dirname(name) GS.pro_last_dir = os.path.basename(GS.pro_dir) @@ -483,6 +490,11 @@ def check_sch(): if not GS.sch_file: GS.exit_with_error('No SCH file found (*.sch), use -e to specify one.', EXIT_BAD_ARGS) + @staticmethod + def check_pro(): + if not GS.pro_file: + GS.exit_with_error('No project file found (*.kicad_pro/*.pro).', EXIT_BAD_ARGS) + @staticmethod def copy_project(new_pcb_name, dry=False): pro_name = GS.pro_file @@ -510,6 +522,18 @@ def copy_project(new_pcb_name, dry=False): copy2(dru_name, dru_copy) return pro_copy, prl_copy, dru_copy + @staticmethod + def copy_project_names(pcb_name): + pro_copy, prl_copy, dru_copy = GS.copy_project(pcb_name, dry=True) + files = [] + if pro_copy: + files.append(pro_copy) + if prl_copy: + files.append(prl_copy) + if dru_copy: + files.append(dru_copy) + return files + @staticmethod def copy_project_sch(sch_dir): """ Copy the project file to the temporal dir """ @@ -819,3 +843,15 @@ def create_fp_lib(lib_name): # This is why we just use os.makedirs os.makedirs(lib_name, exist_ok=True) return lib_name + + @staticmethod + @contextmanager + def create_file(name, bin=False): + os.makedirs(os.path.dirname(name), exist_ok=True) + with open(name, 'wb' if bin else 'w') as f: + yield f + + @staticmethod + def write_to_file(content, name): + with GS.create_file(name, bin=isinstance(content, bytes)) as f: + f.write(content) diff --git a/kibot/out_kicanvas.py b/kibot/out_kicanvas.py new file mode 100644 index 000000000..df7370086 --- /dev/null +++ b/kibot/out_kicanvas.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2024 Salvador E. Tropea +# Copyright (c) 2024 Instituto Nacional de Tecnología Industrial +# License: AGPL-3.0 +# Project: KiBot (formerly KiPlot) +import requests +import os +from .error import KiPlotConfigurationError +from .gs import GS +from .out_base import VariantOptions +from .macros import macros, document, output_class # noqa: F401 +from . import log + +logger = log.get_logger() +VALID_SOURCE = {'schematic', 'pcb', 'project'} +URL_SCRIPT = 'https://kicanvas.org/kicanvas/kicanvas.js' +SCRIPT_NAME = 'kicanvas.js' + + +class KiCanvasOptions(VariantOptions): + def __init__(self): + with document: + self.source = 'schematic' + """ *[string|list(string)] [schematic,pcb,project] Source to display """ + self.local_script = True + """ *Download the script and use a copy """ + self.title = '' + """ Text used to replace the sheet title. %VALUE expansions are allowed. + If it starts with `+` the text is concatenated """ + self.url_script = URL_SCRIPT + """ URL for the KiCanvas script """ + self.controls = 'full' + """ [full,basic,none] Which controls are displayed """ + self.download = True + """ Show the download button """ + self.overlay = True + """ Show the overlay asking to click """ + super().__init__() + self._expand_ext = 'html' + self._expand_id = 'kicanvas' + + def config(self, parent): + super().config(parent) + self.source = self.force_list(self.source, lower_case=True) + for s in self.source: + if s not in VALID_SOURCE: + raise KiPlotConfigurationError(f'Invalid source `{s}` must be any of: {", ".join(VALID_SOURCE)}') + + def get_html_name(self, out_dir): + return os.path.join(out_dir, self.expand_filename_sch(self._parent.output)) + + def _get_targets(self, out_dir, only_index=False): + files = [self.get_html_name(out_dir)] + if only_index: + return files + if self.local_script: + files.append(os.path.join(out_dir, SCRIPT_NAME)) + for s in self.source: + if s == 'pcb': + files.append(os.path.join(out_dir, GS.pcb_fname)) + elif s == 'schematic': + files.extend(GS.sch.file_names_variant(out_dir)) + else: + files.extend(GS.copy_project_names(GS.pcb_file)) + return files + + def get_targets(self, out_dir): + return self._get_targets(out_dir) + + def get_navigate_targets(self, out_dir): + """ Targets for the navigate results, just the index """ + return self._get_targets(out_dir, True) + + def save_pcb(self, out_dir): + out_pcb = os.path.join(out_dir, GS.pcb_fname) + if self._pcb_saved: + return out_pcb + self._pcb_saved = True + self.set_title(self.title) + self.filter_pcb_components(do_3D=True) + logger.debug('Saving PCB to '+out_pcb) + GS.board.Save(out_pcb) + self.unfilter_pcb_components(do_3D=True) + self.restore_title() + return out_pcb + + def save_sch(self, out_dir): + if self._sch_saved: + return + self._sch_saved = True + self.set_title(self.title, sch=True) + logger.debug('Saving Schematic to '+out_dir) + GS.sch.save_variant(out_dir) + self.restore_title(sch=True) + + def run(self, out_dir): + # Download KiCanvas + if self.local_script: + logger.debug(f'Downloading the script from `{self.url_script}`') + try: + r = requests.get(self.url_script, allow_redirects=True) + except Exception as e: + raise KiPlotConfigurationError(f'Failed to download the KiCanvas script from `{self.url_script}`: '+str(e)) + dest = os.path.join(out_dir, SCRIPT_NAME) + logger.debug(f'Saving the script to `{dest}`') + GS.write_to_file(r.content, dest) + script_src = SCRIPT_NAME + else: + script_src = self.url_script + # Generate all pages + self._sch_saved = self._pcb_saved = False + for s in self.source: + # Save the PCB/SCH/Project + if s == 'pcb': + GS.check_pcb() + self.save_pcb(out_dir) + elif s == 'schematic': + GS.check_sch() + self.save_sch(out_dir) + else: + GS.check_sch() + GS.check_pcb() + GS.check_pro() + self.save_sch(out_dir) + GS.copy_project(self.save_pcb(out_dir)) + # Create the HTML file + full_name = self.get_html_name(out_dir) + logger.debug(f'Creating KiCanvas HTML: {full_name}') + controlslist = [] + if not self.download: + controlslist.append('nodownload') + if not self.overlay: + controlslist.append('nooverlay') + controlslist = f' controlslist="{",".join(controlslist)}"' if controlslist else '' + with GS.create_file(full_name) as f: + f.write('\n') + f.write('\n') + f.write(' \n') + f.write(f' \n') + f.write(f' \n') + for s in self.source: + if s == 'pcb': + source = GS.pcb_fname + elif s == 'schematic': + source = GS.sch_fname + else: + source = GS.pro_fname + f.write(f' \n') + f.write(' \n') + f.write(' \n') + f.write('\n') + + +@output_class +class KiCanvas(BaseOutput): # noqa: F821 + """ KiCanvas + Generates an interactive web page to browse the schematic and/or PCB. + """ + def __init__(self): + super().__init__() + self._category = ['PCB/docs', 'Schematic/docs'] + self._both_related = True + with document: + self.output = GS.def_global_output + """ *Filename for the output (%i=kicanvas, %x=html) """ + self.options = KiCanvasOptions + """ *[dict] Options for the KiCanvas output """ + + @staticmethod + def get_conf_examples(name, layers): + # TODO: implement + outs = [] + return outs + + def get_navigate_targets(self, out_dir): + return (self.options.get_navigate_targets(out_dir), None) + # [os.path.join(GS.get_resource_path('kicanvas'), 'images', 'icon.svg')] diff --git a/tests/yaml_samples/kicanvas_1.kibot.yaml b/tests/yaml_samples/kicanvas_1.kibot.yaml new file mode 100644 index 000000000..01fbf5caf --- /dev/null +++ b/tests/yaml_samples/kicanvas_1.kibot.yaml @@ -0,0 +1,15 @@ +# Example KiBot config file +kibot: + version: 1 + +outputs: + - name: 'KiCanvas' + comment: "Example of KiCanvas export" + type: kicanvas + dir: KiCanvas + options: + source: ['schematic', 'pcb', 'project'] + overlay: false + # local_script: false + # source: ['schematic', 'pcb', 'project', 'pp'] + # source: pp