KiBot/kibot/out_kicanvas.py

251 lines
9.7 KiB
Python

# -*- 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 .kiplot import load_sch, load_board
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'
JS_TEST = """
function ready()
{
try
{
var xmlhttp = new XMLHttpRequest();
xmlhttp.open('GET', 'fname', false);
xmlhttp.send();
}
catch (error)
{
if (window.location.protocol === 'file:')
{
document.getElementById('no_file_access').style.display = 'block';
}
throw(error);
}
}
window.addEventListener('DOMContentLoaded', ready);
"""
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' and GS.pcb_fname:
files.append(os.path.join(out_dir, GS.pcb_fname))
elif s == 'schematic' and GS.sch_file:
files.extend(GS.sch.file_names_variant(out_dir))
elif s == 'project' and (GS.sch_file or GS.pcb_file):
files.extend(GS.copy_project_names(GS.sch_file or GS.pcb_file, ref_dir=out_dir))
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):
# We declare _any_related=True because we don't know if the PCB/SCH are needed until the output is configured.
# Now that we know it we can load the PCB and/or SCH.
for s in self.source:
if s == 'pcb':
load_board()
elif s == 'schematic':
load_sch()
else:
load_sch()
load_board()
GS.check_pro()
# Now that we loaded the needed stuff we can do the parent run
super().run(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':
self.save_pcb(out_dir)
elif s == 'schematic':
self.save_sch(out_dir)
else:
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')
# Message to inform we can't read the local files
f.write(' <div id="no_file_access" style="display: none; ">\n')
f.write(" <span>The browser can't read local files. Enable it to continue."
" I.e. use <i>--allow-file-access-from-files</i> on Chrome</span>\n")
f.write(' </div>\n')
# End of message
f.write(f' <kicanvas-embed controls="{self.controls}"{controlslist}>\n')
fname = None
for s in self.source:
if s == 'pcb':
f.write(f' <kicanvas-source src="{GS.pcb_fname}"></kicanvas-source>\n')
fname = GS.pcb_fname
elif s == 'schematic':
GS.sch_dir
for s in sorted(GS.sch.all_sheets, key=lambda x: x.sheet_path_h):
fname = os.path.relpath(s.fname, GS.sch_dir)
f.write(f' <kicanvas-source src="{fname}"></kicanvas-source>\n')
else:
f.write(f' <kicanvas-source src="{GS.pro_fname}"></kicanvas-source>\n')
fname = GS.pro_fname
f.write(' </kicanvas-embed>\n')
# Add test to check if we can read the files
f.write(' <script>\n')
f.write(JS_TEST.replace('fname', fname))
f.write(' </script>\n')
# End of test
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.
Note that this tool is in alpha stage, so be ready to face some issues.
Also note that most browsers won't allow Java Script to read local files,
needed to load the SCH/PCB. So you must use a web server or enable the
access to local files. In the case of Google Chrome you can use the
`--allow-file-access-from-files` command line option.
For more information visit the [KiCanvas web](https://github.com/theacodes/kicanvas)
"""
def __init__(self):
super().__init__()
self._category = ['PCB/docs', 'Schematic/docs']
self._any_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 """
def config(self, parent):
super().config(parent)
if self.get_user_defined('category'):
# The user specified a category, don't change it
return
# Adjust the category according to the selected output/s
cat = set()
for s in self.options.source:
if s == 'pcb':
cat.add('PCB/docs')
elif s == 'schematic':
cat.add('Schematic/docs')
else:
cat.add('PCB/docs')
cat.add('Schematic/docs')
self.category = list(cat)
@staticmethod
def get_conf_examples(name, layers):
outs = BaseOutput.simple_conf_examples(name, 'Web page to browse the schematic and/or PCB', 'Browse') # noqa: F821
sources = []
if GS.sch_file:
sources.append('schematic')
if GS.pcb_file:
sources.append('pcb')
outs[0]['options'] = {'source': sources}
return outs
def get_navigate_targets(self, out_dir):
return (self.options.get_navigate_targets(out_dir), [os.path.join(GS.get_resource_path('kicanvas'), 'kicanvas.svg')])