[KiCanvas][Added] Some very raw support

Closes #572
This commit is contained in:
Salvador E. Tropea 2024-01-26 13:04:27 -03:00
parent c14019cc25
commit de1fc05298
8 changed files with 331 additions and 5 deletions

View File

@ -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

View File

@ -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).

View File

@ -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

View File

@ -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:`: <pair: output - kicanvas; comment>` [string=''] A comment for documentation purposes. It helps to identify the output.
- **dir** :index:`: <pair: output - kicanvas; dir>` [string='./'] Output directory for the generated files.
If it starts with `+` the rest is concatenated to the default dir.
- **name** :index:`: <pair: output - kicanvas; name>` [string=''] Used to identify this particular output definition.
Avoid using `_` as first character. These names are reserved for KiBot.
- **options** :index:`: <pair: output - kicanvas; options>` [dict] Options for the KiCanvas output.
- Valid keys:
- **local_script** :index:`: <pair: output - kicanvas - options; local_script>` [boolean=true] Download the script and use a copy.
- **source** :index:`: <pair: output - kicanvas - options; source>` [string|list(string)] [schematic,pcb,project] Source to display.
- ``controls`` :index:`: <pair: output - kicanvas - options; controls>` [string='full'] [full,basic,none] Which controls are displayed.
- ``dnf_filter`` :index:`: <pair: output - kicanvas - options; 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.
- ``download`` :index:`: <pair: output - kicanvas - options; download>` [boolean=true] Show the download button.
- ``overlay`` :index:`: <pair: output - kicanvas - options; overlay>` [boolean=true] Show the overlay asking to click.
- ``pre_transform`` :index:`: <pair: output - kicanvas - options; pre_transform>` [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:`: <pair: output - kicanvas - options; title>` [string=''] Text used to replace the sheet title. %VALUE expansions are allowed.
If it starts with `+` the text is concatenated.
- ``url_script`` :index:`: <pair: output - kicanvas - options; url_script>` URL for the KiCanvas script.
- ``variant`` :index:`: <pair: output - kicanvas - options; variant>` [string=''] Board variant to apply.
- **output** :index:`: <pair: output - kicanvas; output>` [string='%f-%i%I%v.%x'] Filename for the output (%i=kicanvas, %x=html). Affected by global options.
- **type** :index:`: <pair: output - kicanvas; type>` 'kicanvas'
- ``category`` :index:`: <pair: output - kicanvas; 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**.
The categories are currently used for `navigate_results`.
- ``disable_run_by_default`` :index:`: <pair: output - kicanvas; 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`` :index:`: <pair: output - kicanvas; extends>` [string=''] Copy the `options` section from the indicated output.
Used to inherit options from another output of the same type.
- ``groups`` :index:`: <pair: output - kicanvas; groups>` [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:`: <pair: output - kicanvas; output_id>` [string=''] Text to use for the %I expansion content. To differentiate variations of this output.
- ``priority`` :index:`: <pair: output - kicanvas; 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`` :index:`: <pair: output - kicanvas; run_by_default>` [boolean=true] When enabled this output will be created when no specific outputs are requested.

View File

@ -29,6 +29,7 @@ Notes:
outputs/ibom
outputs/info
outputs/kibom
outputs/kicanvas
outputs/kicost
outputs/kikit_present
outputs/kiri

View File

@ -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 @@ class GS(object):
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 @@ class GS(object):
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 @@ class GS(object):
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 @@ class GS(object):
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 @@ class GS(object):
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 @@ class GS(object):
# 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)

177
kibot/out_kicanvas.py Normal file
View File

@ -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('<!DOCTYPE HTML>\n')
f.write('<html lang="en">\n')
f.write(' <body>\n')
f.write(f' <script type="module" src="{script_src}"></script>\n')
f.write(f' <kicanvas-embed controls="{self.controls}"{controlslist}>\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' <kicanvas-source src="{source}"></kicanvas-source>\n')
f.write(' </kicanvas-embed>\n')
f.write(' </body>\n')
f.write('</html>\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')]

View File

@ -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