diff --git a/kibot/out_kiri.py b/kibot/out_kiri.py
new file mode 100644
index 00000000..ca4bae99
--- /dev/null
+++ b/kibot/out_kiri.py
@@ -0,0 +1,365 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2022-2023 Salvador E. Tropea
+# Copyright (c) 2022-2023 Instituto Nacional de TecnologĂa Industrial
+# License: GPL-3.0
+# Project: KiBot (formerly KiPlot)
+"""
+Dependencies:
+ - name: KiCad PCB/SCH Diff
+ version: 2.5.0
+ role: mandatory
+ github: INTI-CMNB/KiDiff
+ command: kicad-diff.py
+ pypi: kidiff
+ downloader: pytool
+ id: KiDiff
+ - from: Git
+ role: Compare with files in the repo
+ - from: KiAuto
+ role: Compare schematics
+ version: 2.2.0
+"""
+import datetime
+import pwd
+import os
+from shutil import copy2
+from subprocess import CalledProcessError
+from tempfile import mkdtemp, NamedTemporaryFile
+from .error import KiPlotConfigurationError
+from .gs import GS
+from .kicad.color_theme import load_color_theme
+from .kiplot import load_any_sch, run_command
+from .layer import Layer
+from .out_base import VariantOptions
+from .macros import macros, document, output_class # noqa: F401
+from . import log
+
+logger = log.get_logger()
+STASH_MSG = 'KiBot_Changes_Entry'
+TOOLTIP_HTML = '
Commit: {hash}Date: {dt}Author: {author}Description:{desc}
'
+# Icons for modified status
+EMPTY_IMG = ('')
+SCH_IMG = ('')
+PCB_IMG = ('')
+TXT_IMG = ('')
+HASH_LOCAL = '_local_'
+
+
+def get_cur_user():
+ try:
+ name = pwd.getpwuid(os.geteuid())[4]
+ return name.split(',')[0]
+ except Exception:
+ return 'Local user'
+
+
+class KiRiOptions(VariantOptions):
+ def __init__(self):
+ with document:
+ self.output = GS.def_global_output
+ """ *Filename for the output (%i=diff_pcb/diff_sch, %x=pdf) """
+ self.color_theme = '_builtin_classic'
+ """ *Selects the color theme. Only applies to KiCad 6.
+ To use the KiCad 6 default colors select `_builtin_default`.
+ Usually user colors are stored as `user`, but you can give it another name """
+ super().__init__()
+ self._expand_id = 'diff'
+ self._expand_ext = 'pdf'
+
+ def get_targets(self, out_dir):
+ # TODO: Implement
+ return [self._parent.expand_filename(out_dir, self.output)]
+
+ def add_to_cache(self, name, hash):
+ cmd = [self.command, '--no_reader', '--only_cache', '--old_file_hash', hash[:7], '--cache_dir', self.cache_dir,
+ '--kiri_mode', '--all_pages']
+ if self.incl_file:
+ cmd.extend(['--layers', self.incl_file])
+ if GS.debug_enabled:
+ cmd.insert(1, '-'+'v'*GS.debug_level)
+ cmd.extend([name, name])
+ self.name_used_for_cache = name
+ run_command(cmd)
+
+ def run_git(self, cmd, cwd=None, just_raise=False):
+ if cwd is None:
+ cwd = self.repo_dir
+ return run_command([self.git_command]+cmd, change_to=cwd, just_raise=just_raise)
+
+ def git_dirty(self, file):
+ return self.run_git(['status', '--porcelain', '-uno', file])
+
+ def remove_git_worktree(self, name):
+ logger.debug('Removing temporal checkout at '+name)
+ self.run_git(['worktree', 'remove', '--force', name])
+
+ def create_layers_incl(self, layers):
+ self.incl_file = None
+ if not isinstance(layers, type):
+ layers = Layer.solve(layers)
+ # TODO no list (ALL)
+ self._solved_layers = layers
+ logger.debug('Including layers:')
+ with NamedTemporaryFile(mode='w', suffix='.lst', delete=False) as f:
+ self.incl_file = f.name
+ for la in layers:
+ logger.debug('- {} ({})'.format(la.layer, la.id))
+ f.write(str(la.id)+'\n')
+
+ def do_cache(self, name, tmp_wd, hash):
+ name_copy = self.run_git(['ls-files', '--full-name', name])
+ name_copy = os.path.join(tmp_wd, name_copy)
+ logger.debug('- Using temporal copy: '+name_copy)
+ self.add_to_cache(name_copy, hash)
+ return name_copy
+
+ def save_pcb_layers(self, hash=None):
+ subdir = os.path.join(hash[:7], '_KIRI_') if hash is not None else ''
+ with open(os.path.join(self.cache_dir, subdir, 'pcb_layers'), 'wt') as f:
+ for la in self._solved_layers:
+ f.write(str(la.id)+'|'+la.layer+'\n')
+
+ def solve_layer_colors(self):
+ # Color theme
+ self._color_theme = load_color_theme(self.color_theme)
+ if self._color_theme is None:
+ raise KiPlotConfigurationError("Unable to load `{}` color theme".format(self.color_theme))
+ # Assign a color if none was defined
+ layer_id2color = self._color_theme.layer_id2color
+ for la in self._solved_layers:
+ if la._id in layer_id2color:
+ la.color = layer_id2color[la._id]
+ else:
+ la.color = "#000000"
+
+ def create_layers(self, f):
+ template = self.load_html_template('layers', 11)
+ for i, la in enumerate(self._solved_layers):
+ # TODO: Configure checked?
+ checked = 'checked="checked"' if i == 0 else ''
+ f.write(template.format(i=i+1, layer_id_padding='%02d' % (i+1), layer_name=la.suffix,
+ layer_id=la.id, layer_color=la.color, checked=checked))
+
+ def save_sch_sheet(self, hash, name_sch):
+ # Load the schematic. Really worth?
+ sch = load_any_sch(name_sch, GS.sch_basename)
+ with open(os.path.join(self.cache_dir, hash[:7], '_KIRI_', 'sch_sheets'), 'wt') as f:
+ base_dir = os.path.dirname(name_sch)
+ for s in sch.all_sheets:
+ fname = s.fname
+ no_ext = os.path.splitext(os.path.basename(fname))[0]
+ rel_name = os.path.relpath(fname, base_dir)
+ if s.sheet_path_h == '/':
+ instance_name = sheet_path = GS.sch_basename
+ else:
+ instance_name = os.path.basename(s.sheet_path_h)
+ sheet_path = s.sheet_path_h.replace('/', '-')
+ sheet_path = GS.sch_basename+'-'+sheet_path[1:]
+ f.write(f'{no_ext}|{rel_name}||{instance_name}|{sheet_path}\n')
+
+ def create_pages(self, f):
+ template = self.load_html_template('pages', 11)
+ for i, s in enumerate(sorted(GS.sch.all_sheets, key=lambda s: s.sheet_path_h)):
+ fname = s.fname
+ checked = 'checked="checked"' if i == 0 else ''
+ base_name = os.path.basename(fname)
+ rel_name = os.path.relpath(fname, GS.sch_dir)
+ if s.sheet_path_h == '/':
+ instance_name = sheet_path = GS.sch_basename
+ else:
+ instance_name = os.path.basename(s.sheet_path_h)
+ sheet_path = s.sheet_path_h.replace('/', '-')
+ sheet_path = GS.sch_basename+'-'+sheet_path[1:]
+ f.write(template.format(i=i+1, page_name=instance_name, page_filename_path=rel_name,
+ page_filename=base_name, checked=checked))
+
+ def load_html_template(self, type, tabs):
+ """ Load a template used to generate an HTML section.
+ Outside of the code for easier modification/customization. """
+ with open(os.path.join(GS.get_resource_path('kiri'), f'{type}_template.html'), 'rt') as f:
+ template = f.read()
+ template = template.replace('${', '{')
+ template = template.replace('$(printf "%02d" {i})', '{i02}')
+ template = template.replace('{class}', '{cls}')
+ template = template.replace('\t\t', '\t'*tabs)
+ return template
+
+ def create_index(self, commits):
+ # Get the KiRi template
+ with open(os.path.join(GS.get_resource_path('kiri'), 'index.html'), 'rt') as f:
+ template = f.read()
+ today = datetime.datetime.today().strftime('%Y-%m-%d')
+ # Replacement keys
+ rep = {}
+ rep['PROJECT_TITLE'] = GS.pro_basename or GS.sch_basename or GS.pcb_basename or 'unknown'
+ rep['SCH_TITLE'] = GS.sch_title or 'No title'
+ rep['SCH_REVISION'] = GS.sch_rev or ''
+ rep['SCH_DATE'] = GS.sch_date or today
+ rep['PCB_TITLE'] = GS.pcb_title or 'No title'
+ rep['PCB_REVISION'] = GS.pcb_rev or ''
+ rep['PCB_DATE'] = GS.pcb_date or today
+ # Fill the template
+ with open(os.path.join(self.cache_dir, 'web', 'index.html'), 'wt') as f:
+ for ln in iter(template.splitlines()):
+ for k, v in rep.items():
+ ln = ln.replace(f'[{k}]', v)
+ f.write(ln+'\n')
+ if ln.endswith(''):
+ self.create_commits(f, commits)
+ elif ln.endswith(''):
+ self.create_pages(f)
+ elif ln.endswith(''):
+ self.create_layers(f)
+
+ def create_commits(self, f, commits):
+ template = self.load_html_template('commits', 8)
+ for i, c in enumerate(commits):
+ hash = c[0][:7]
+ dt = c[1].split()[0]
+ author = c[2]+' '
+ desc = c[3]
+ tooltip = TOOLTIP_HTML.format(hash=hash, dt=dt, author=author, desc=desc)
+ cls = 'text-warning' if hash == HASH_LOCAL else 'text-info'
+ icon_pcb = PCB_IMG if c[0] in self.commits_with_changed_pcb else EMPTY_IMG
+ icon_sch = SCH_IMG if c[0] in self.commits_with_changed_sch else EMPTY_IMG
+ # TODO What's this? if we only track changes in PCB/Sch this should be empty
+ icon_txt = TXT_IMG
+ f.write(template.format(i=i+1, hash=hash, tooltip=tooltip, text=c[3], cls=cls, i02='%02d' % (i+1),
+ date=dt, user=author, pcb_icon=icon_pcb, sch_icon=icon_sch, txt_icon=icon_txt, hash_label=hash))
+
+ def get_modified_status(self, pcb_file, sch_files):
+ res = self.run_git(['log', '--pretty=format:%H', '--', pcb_file])
+ self.commits_with_changed_pcb = set(res.split())
+ res = self.run_git(['log', '--pretty=format:%H', '--'] + sch_files)
+ self.commits_with_changed_sch = set(res.split())
+ if GS.debug_level > 1:
+ logger.debug(f'Commits with changes in the PCB: {self.commits_with_changed_pcb}')
+ logger.debug(f'Commits with changes in the Schematics: {self.commits_with_changed_sch}')
+
+ def create_kiri_files(self):
+ src_dir = GS.get_resource_path('kiri')
+ copy2(os.path.join(src_dir, 'redirect.html'), os.path.join(self.cache_dir, 'index.html'))
+ copy2(os.path.join(src_dir, 'kiri-server'), os.path.join(self.cache_dir, 'kiri-server'))
+ web_dir = os.path.join(self.cache_dir, 'web')
+ os.makedirs(web_dir, exist_ok=True)
+ copy2(os.path.join(src_dir, 'blank.svg'), os.path.join(web_dir, 'blank.svg'))
+ copy2(os.path.join(src_dir, 'favicon.ico'), os.path.join(web_dir, 'favicon.ico'))
+ copy2(os.path.join(src_dir, 'kiri.css'), os.path.join(web_dir, 'kiri.css'))
+ copy2(os.path.join(src_dir, 'kiri.js'), os.path.join(web_dir, 'kiri.js'))
+
+ def run(self, name):
+ self.cache_dir = self._parent.output_dir
+ self.command = self.ensure_tool('KiDiff')
+ self.git_command = self.ensure_tool('Git')
+ # Get a list of files for the project
+ GS.check_sch()
+ sch_files = GS.sch.get_files()
+ self.repo_dir = GS.sch_dir
+ GS.check_pcb()
+ # Get a list of hashes where we have changes
+ # TODO implement a limit -n X
+ res = self.run_git(['log', "--date=format:%Y-%m-%d %H:%M:%S", '--pretty=format:%H | %ad | %an | %s', '--',
+ GS.pcb_file] + sch_files)
+ hashes = [r.split(' | ') for r in res.split('\n')]
+ self.create_layers_incl(self.layers)
+ self.solve_layer_colors()
+ # Get more information about what is changed
+ self.get_modified_status(GS.pcb_file, sch_files)
+ # TODO ensure we have at least 2
+ try:
+ git_tmp_wd = None
+ try:
+ for h in hashes:
+ hash = h[0]
+ git_tmp_wd = mkdtemp()
+ logger.debug('Checking out '+hash+' to '+git_tmp_wd)
+ self.run_git(['worktree', 'add', git_tmp_wd, hash])
+ self.run_git(['submodule', 'update', '--init', '--recursive'], cwd=git_tmp_wd)
+ # Generate SVGs for the schematic
+ name_sch = self.do_cache(GS.sch_file, git_tmp_wd, hash)
+ # Generate SVGs for the PCB
+ self.do_cache(GS.pcb_file, git_tmp_wd, hash)
+ # List of layers
+ self.save_pcb_layers(hash)
+ # Schematic hierarchy
+ self.save_sch_sheet(hash, name_sch)
+ self.remove_git_worktree(git_tmp_wd)
+ git_tmp_wd = None
+ finally:
+ if git_tmp_wd:
+ self.remove_git_worktree(git_tmp_wd)
+ # Do we have modifications?
+ sch_dirty = self.git_dirty(GS.sch_file)
+ pcb_dirty = self.git_dirty(GS.pcb_file)
+ if sch_dirty or pcb_dirty:
+ # Include the current files
+ name_sch = self.do_cache(GS.sch_file, GS.sch_dir, HASH_LOCAL)
+ self.save_sch_sheet(HASH_LOCAL, name_sch)
+ self.do_cache(GS.pcb_file, GS.pcb_dir, HASH_LOCAL)
+ self.save_pcb_layers(HASH_LOCAL)
+ hashes.insert(0, (HASH_LOCAL, datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S'), get_cur_user(),
+ 'Local changes not committed'))
+ if pcb_dirty:
+ self.commits_with_changed_pcb.add(HASH_LOCAL)
+ if sch_dirty:
+ self.commits_with_changed_sch.add(HASH_LOCAL)
+ finally:
+ if self.incl_file:
+ os.remove(self.incl_file)
+ self.create_kiri_files()
+ self.create_index(hashes)
+
+
+@output_class
+class KiRi(BaseOutput): # noqa: F821
+ """ KiRi
+ Generates a PDF with the differences between two PCBs or schematics.
+ Recursive git submodules aren't supported (submodules inside submodules) """
+ def __init__(self):
+ super().__init__()
+ self._category = ['PCB/docs', 'Schematic/docs']
+ self._both_related = True
+ with document:
+ self.options = KiRiOptions
+ """ *[dict] Options for the `diff` output """
+ self.layers = Layer
+ """ *[list(dict)|list(string)|string] [all,selected,copper,technical,user]
+ List of PCB layers to use. When empty all available layers are used.
+ Note that if you want to support adding/removing layers you should specify a list here """
+
+ @staticmethod
+ def layer2dict(la):
+ return {'layer': la.layer, 'suffix': la.suffix, 'description': la.description}
+
+ @staticmethod
+ def has_repo(git_command, file):
+ try:
+ run_command([git_command, 'ls-files', '--error-unmatch', file], change_to=os.path.dirname(file), just_raise=True)
+ except CalledProcessError:
+ logger.debug("File `{}` not inside a repo".format(file))
+ return False
+ return True
+
+ @staticmethod
+ def get_conf_examples(name, layers):
+ outs = []
+ git_command = GS.check_tool(name, 'Git')
+ # TODO: Implement
+ if (GS.pcb_file and GS.sch_file and KiRi.has_repo(git_command, GS.pcb_file) and
+ KiRi.has_repo(git_command, GS.sch_file)):
+ gb = {}
+ gb['name'] = 'basic_{}'.format(name)
+ gb['comment'] = 'Interactive diff between commits'
+ gb['type'] = name
+ gb['dir'] = 'diff'
+ gb['layers'] = [KiRi.layer2dict(la) for la in layers]
+ outs.append(gb)
+ return outs
+
+ def run(self, name):
+ self.options.layers = self.layers
+ super().run(name)
diff --git a/kibot/resources/kiri/blank.svg b/kibot/resources/kiri/blank.svg
new file mode 100644
index 00000000..747e878b
--- /dev/null
+++ b/kibot/resources/kiri/blank.svg
@@ -0,0 +1,55 @@
+
+
+
+
diff --git a/kibot/resources/kiri/commits_template.html b/kibot/resources/kiri/commits_template.html
new file mode 100644
index 00000000..6d347e3f
--- /dev/null
+++ b/kibot/resources/kiri/commits_template.html
@@ -0,0 +1,21 @@
+
+
+
diff --git a/kibot/resources/kiri/favicon.ico b/kibot/resources/kiri/favicon.ico
new file mode 100644
index 00000000..0e1fef16
Binary files /dev/null and b/kibot/resources/kiri/favicon.ico differ
diff --git a/kibot/resources/kiri/index.html b/kibot/resources/kiri/index.html
new file mode 100644
index 00000000..a23957b1
--- /dev/null
+++ b/kibot/resources/kiri/index.html
@@ -0,0 +1,319 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ [PROJECT_TITLE]
+
+
+
+
+
+
+
+
+
+
+
+
Kicad Revision Inspector
+
+
[SCH_TITLE]
+
Rev. [SCH_REVISION] ([SCH_DATE])
+
+
+
[PCB_TITLE]
+
Rev. [PCB_REVISION] ([PCB_DATE])
+
+
+
+ Commits
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Newer
+
+
+
+
+
+
+
+ Older
+
+
+
+
+
+
+ Unchanged
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Commits List
+
+
+
+
+ |
+ Move commits par upwards |
+
+
+ |
+ Move commits par downwards |
+
+
+ | [ or Ctrl + |
+ Move 2nd commit upwards |
+
+
+ | ] or Ctrl + |
+ Move 2nd commit downwards |
+
+
+ | r |
+ Reset commit selection to the top 2 commits |
+
+
+
+
+
+
View, sheets and layers
+
+
+
+
+ | s |
+ Switch Schematic/Layout view |
+
+
+ | q |
+ Toggle visibility of newer commit |
+
+
+ | w |
+ Toggle visibility of older commit |
+
+
+ |
+ Select next page/layer |
+
+
+ |
+ Select previous page/layer |
+
+
+ | Ctrl + |
+ Select next page/layer (cycling) |
+
+
+ | Ctrl + |
+ Select previous page/layer (cycling) |
+
+
+
+
+
+
Diff Pan and Zoom
+
+
+
+
+ | + |
+ Zoom in |
+
+
+ | - |
+ Zoom out |
+
+
+ | 0 |
+ Zoom fit |
+
+
+ | Alt + |
+ Pan svg up |
+
+
+ | Alt + |
+ Pan svg down |
+
+
+ | Alt + |
+ Pan svg left |
+
+
+ | Alt + |
+ Pan svg right |
+
+
+
+
+
+
Miscellaneous
+
+
+
+
+ | f |
+ Toggle full screen view |
+
+
+ | i |
+ Shows this info view |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/kibot/resources/kiri/kiri-server b/kibot/resources/kiri/kiri-server
new file mode 100755
index 00000000..49ecd037
--- /dev/null
+++ b/kibot/resources/kiri/kiri-server
@@ -0,0 +1,204 @@
+#!/usr/bin/env python3
+
+import argparse
+import os
+import shutil
+import re
+import sys
+import signal
+import subprocess
+
+from http.server import SimpleHTTPRequestHandler
+
+import webbrowser
+import socketserver
+
+from subprocess import PIPE, Popen
+from typing import List, Tuple
+
+from urllib.parse import unquote
+
+www_dir_path = None
+httpd = None
+default_port = 8080
+
+
+class WebServerHandler(SimpleHTTPRequestHandler):
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(
+ *args, directory=os.path.realpath(www_dir_path, **kwargs)
+ )
+
+ def log_message(self, format, *args):
+ pass
+
+ def do_GET(self):
+ super().do_GET()
+
+ def do_POST(self):
+ try:
+
+ self.send_response(200)
+ # self.send_header('Location', self.path)
+ self.end_headers()
+
+ content_len = int(self.headers.get('content-length', 0))
+ post_body = unquote(self.rfile.read(content_len)).split("&")
+
+ # with open("post.txt","a") as f:
+ # f.write(str(post_body))
+ # f.write("\n")
+
+ commit_hash = post_body[0].split("=")[1]
+ kicad_pro_path = post_body[1].split("=")[1]
+
+ cmd = "kicad_rev {}".format(kicad_pro_path)
+ print("Cmd:", cmd)
+ process = subprocess.Popen(cmd.split(" "), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+
+ # self._set_response()
+ # self.wfile.write("POST request for {}".format(self.path).encode('utf-8'))
+
+ except:
+ pass
+
+def signal_handler(sig, frame):
+ httpd.server_close()
+ sys.exit(0)
+
+
+def parse_cli_args():
+ parser = argparse.ArgumentParser(description="Kicad PCB visual diffs.")
+ parser.add_argument(
+ "-d", "--display", type=str, help="Set DISPLAY value, default :1.0", default=":1.0",
+ )
+ parser.add_argument(
+ "-n", "--nested", action='store_true', help="Fix paths when it was a nested project"
+ )
+ parser.add_argument(
+ "-p", "--port", type=int, help="Force the weverver on an specific port"
+ )
+ parser.add_argument(
+ "-r", "--port_range", type=int, default=10, help="Port range to try"
+ )
+ parser.add_argument(
+ "-S", "--server-only", action='store_true', help="Port range to try"
+ )
+ parser.add_argument(
+ "-w",
+ "--webserver-disable",
+ action="store_true",
+ help="Does not execute webserver (just generate images)",
+ )
+ parser.add_argument(
+ "-v", "--verbose", action="count", default=0, help="Increase verbosity (-vvv)"
+ )
+ parser.add_argument(
+ "-i", "--ip", type=str, help="Override default IP address", default="127.0.0.1",
+ )
+ parser.add_argument(
+ "www_dir", metavar="WWW_DIR", help="A folder with web/index.html inside"
+ )
+
+ args = parser.parse_args()
+
+ if args.verbose >= 3:
+ print("")
+ print("Command Line Arguments")
+ print(args)
+
+ return args
+
+
+def launch_webserver(ip, request_handler, port, kicad_project, server_only):
+
+ global httpd
+ httpd = socketserver.TCPServer(("", port), request_handler)
+
+ with httpd:
+ if args.nested:
+ url = "{ip}:{port}/{nested_project}/web/index.html".format(
+ ip=ip,
+ port=str(port),
+ nested_project=kicad_project
+ )
+ else:
+ url = "http://{ip}:{port}/web/index.html".format(
+ ip=ip,
+ port=str(port)
+ )
+
+ print("")
+ print("Starting webserver at {}".format(url))
+ print("(Hit Ctrl+C to exit)")
+ if not server_only:
+ webbrowser.open(url)
+ httpd.serve_forever()
+
+
+def run_cmd(path: str, cmd: List[str]) -> Tuple[str, str]:
+
+ p = Popen(
+ cmd,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ close_fds=True,
+ encoding="utf-8",
+ cwd=path,
+ )
+
+ stdout, stderr = p.communicate()
+ p.wait()
+
+ return stdout.strip("\n "), stderr
+
+
+if __name__ == "__main__":
+
+ signal.signal(signal.SIGINT, signal_handler)
+ args = parse_cli_args()
+
+ if args.www_dir:
+
+ www_dir_path = os.path.abspath(args.www_dir)
+ kicad_project = ".."
+
+ # Assume it is running outside of the webserver the folder
+ index_html = os.path.realpath(os.path.join(www_dir_path, "web", "index.html"))
+ if not os.path.exists(index_html):
+ print("Could not find index.html")
+ exit(1)
+
+ if args.verbose:
+ print("")
+ print("www_dir_path:", www_dir_path)
+ print("kicad_project:", kicad_project)
+ print("index_html:", index_html)
+ print("")
+
+ else:
+ print("www directory is missing")
+ exit(1)
+
+ if not args.webserver_disable:
+
+ socketserver.TCPServer.allow_reuse_address = True
+ request_handler = WebServerHandler
+
+ if args.port:
+ try:
+ launch_webserver(args.ip, request_handler, args.port, kicad_project, args.server_only)
+ except Exception:
+ print("Specified port {port} is in use".format(port=port))
+ pass
+ else:
+ for i in range(args.port_range):
+ try:
+ port = default_port + i
+ launch_webserver(args.ip, request_handler, port, kicad_project, args.server_only)
+ except Exception:
+ # print("Specified port {port} is in use".format(port=port))
+ pass
+ print("Specified ports are in use.")
diff --git a/kibot/resources/kiri/kiri.css b/kibot/resources/kiri/kiri.css
new file mode 100644
index 00000000..dda8690d
--- /dev/null
+++ b/kibot/resources/kiri/kiri.css
@@ -0,0 +1,342 @@
+
+html, body {
+ height : 100%;
+ margin : 0px;
+ font-size : 1em; /* Normalize view on firefox */
+ line-height : 1.5em; /* Normalize view on firefox */
+ caret-color: transparent;
+}
+
+.fill {
+ min-height: 100%;
+ height: 100%;
+ min-width: 100%;
+ width: 100%;
+}
+
+.no-gutters {
+ margin-right: 0px;
+ margin-left: 0px;
+ > .col,
+ > [class*="col-"] {
+ padding-right: 0px;
+ padding-left: 0px;
+ }
+}
+
+/* ==============================
+ Commits List
+ ==============================
+*/
+
+.list-group-item {
+ user-select: none;
+}
+
+.list-group input[type="checkbox"] {
+ display: none;
+}
+
+.list-group input[type="checkbox"] + .list-group-item {
+ background-color: #222;
+ color: #dbdbdb;
+ font-family: monospace;
+ padding: 1px;
+ margin: 4px;
+ border-radius: 5px;
+}
+
+.list-group input[type="checkbox"] + .list-group-item:before {
+ color: transparent;
+ font-weight: bold;
+}
+
+.list-group input[type="checkbox"]:checked + .list-group-item {
+ background-color: #111;
+ color: #fff;
+ border-radius: 5px;
+}
+
+.list-group input[type="checkbox"]:checked + .list-group-item:before {
+ color: inherit;
+}
+
+/* ==============================
+ Scrollbar
+ ==============================
+*/
+
+.scrollbox {
+ overflow: auto;
+ visibility: hidden;
+}
+
+.scrollbox-content,
+.scrollbox:hover,
+.scrollbox:focus {
+ visibility: visible;
+}
+
+::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+ background: rgba(90, 90, 90);
+}
+
+::-webkit-scrollbar-track {
+ background: rgba(0, 0, 0, 0.2);
+}
+
+/* ==============================
+ Toolbar buttons
+ ==============================
+*/
+
+.btn-group input[type="radio"] {
+ display: none;
+}
+
+.btn-group input[type="radio"] + .btn-group-item {
+ font-family: monospace;
+}
+
+.btn-group input[type="radio"] + .btn-group-item:before {
+}
+
+/* ==============================
+ Layers List
+ ==============================
+*/
+
+.list-group input[type="radio"] {
+ display: none;
+}
+
+.list-group input[type="radio"] + .list-group-item {
+ background-color: #222222;
+ color: #dbdbdb;
+ font-family: monospace;
+ padding: 2px;
+ margin: 4px;
+}
+
+.list-group input[type="radio"] + .list-group-item:before {
+ color: transparent;
+ font-weight: bold;
+}
+
+.list-group input[type="radio"]:checked + .list-group-item {
+ background-color: #111111;
+ color: #FFF;
+}
+
+.list-group input[type="radio"]:checked + .list-group-item:before {
+ color: inherit;
+}
+
+/* ==============================
+ ==============================
+*/
+
+#diff-container {
+/* border: 1px solid #111;*/
+ margin-top: 15px;
+ background-color: #222;
+ border-radius: 5px;
+}
+
+#svg-id {
+/* border: 1px solid #111;*/
+ margin-top: 15px;
+ background-color: #222;
+ border-radius: 5px;
+}
+
+/* ==============================
+ Layers colors
+ ==============================
+*/
+
+.F_Cu {
+ filter : invert(28%) sepia(50%) saturate(2065%) hue-rotate(334deg) brightness(73%) contrast(97%);
+}
+
+.In1_Cu {
+ filter : invert(69%) sepia(39%) saturate(1246%) hue-rotate(17deg) brightness(97%) contrast(104%);
+}
+
+.In2_Cu {
+ filter : invert(14%) sepia(79%) saturate(5231%) hue-rotate(293deg) brightness(91%) contrast(119%);
+}
+
+.In3_Cu {
+ filter : invert(14%) sepia(79%) saturate(5231%) hue-rotate(293deg) brightness(91%) contrast(119%);
+}
+
+.In4_Cu {
+ filter : invert(14%) sepia(79%) saturate(5231%) hue-rotate(293deg) brightness(91%) contrast(119%);
+}
+
+.B_Cu {
+ filter : invert(44%) sepia(14%) saturate(2359%) hue-rotate(70deg) brightness(103%) contrast(82%);
+}
+
+.F_Mask {
+ filter : invert(27%) sepia(51%) saturate(1920%) hue-rotate(269deg) brightness(89%) contrast(96%);
+}
+
+.B_Mask {
+ filter : invert(22%) sepia(56%) saturate(2652%) hue-rotate(277deg) brightness(94%) contrast(87%);
+}
+
+.F_Paste {
+ filter : invert(57%) sepia(60%) saturate(6%) hue-rotate(314deg) brightness(92%) contrast(99%);
+}
+
+.B_Paste {
+ filter : invert(91%) sepia(47%) saturate(4033%) hue-rotate(139deg) brightness(82%) contrast(91%);
+}
+
+.F_SilkS {
+ filter : invert(46%) sepia(44%) saturate(587%) hue-rotate(132deg) brightness(101%) contrast(85%);
+}
+
+.B_SilkS {
+ filter : invert(14%) sepia(27%) saturate(2741%) hue-rotate(264deg) brightness(95%) contrast(102%);
+}
+
+.Edge_Cuts {
+ filter : invert(79%) sepia(79%) saturate(401%) hue-rotate(6deg) brightness(88%) contrast(88%);
+}
+
+.Margin {
+ filter : invert(74%) sepia(71%) saturate(5700%) hue-rotate(268deg) brightness(89%) contrast(84%);
+}
+
+.Dwgs_User {
+ filter : invert(40%) sepia(68%) saturate(7431%) hue-rotate(203deg) brightness(89%) contrast(98%);
+}
+
+.Cmts_User {
+ filter : invert(73%) sepia(10%) saturate(1901%) hue-rotate(171deg) brightness(95%) contrast(102%);
+}
+
+.Eco1_User {
+ filter : invert(25%) sepia(98%) saturate(2882%) hue-rotate(109deg) brightness(90%) contrast(104%);
+}
+
+.Eco2_User {
+ filter : invert(85%) sepia(21%) saturate(5099%) hue-rotate(12deg) brightness(91%) contrast(102%);
+}
+
+.F_Fab {
+ filter : invert(71%) sepia(21%) saturate(4662%) hue-rotate(21deg) brightness(103%) contrast(100%);
+}
+
+.B_Fab {
+ filter : invert(60%) sepia(0%) saturate(0%) hue-rotate(253deg) brightness(87%) contrast(90%);
+}
+
+.F_Adhes {
+ filter : invert(38%) sepia(49%) saturate(1009%) hue-rotate(254deg) brightness(88%) contrast(86%);
+}
+
+.B_Adhes {
+ filter : invert(24%) sepia(48%) saturate(2586%) hue-rotate(218deg) brightness(88%) contrast(92%);
+}
+
+.F_CrtYd {
+ filter : invert(73%) sepia(1%) saturate(0%) hue-rotate(116deg) brightness(92%) contrast(91%);
+}
+
+.B_CrtYd {
+ filter : invert(79%) sepia(92%) saturate(322%) hue-rotate(3deg) brightness(89%) contrast(92%);
+}
+
+/* ==============================
+** ============================*/
+
+.ellipsis {
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow: hidden;
+ display: inline-block;
+}
+
+label#layers, label#pages {
+ margin-top: 0px;
+ margin-bottom: 4px;
+ white-space: nowrap;
+ width: 180px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ vertical-align:top;
+}
+
+#server_offline {
+ position: fixed;
+ display: none;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0,0,0,0.8);
+ z-index: 2;
+ cursor: pointer;
+}
+
+/* ==============================
+** ============================*/
+
+a:link {
+ color: #28a745;
+}
+
+a:visited {
+ color: #28a745;
+}
+
+a:hover {
+ color: #28a745;
+}
+
+a:active {
+ color: #28a745;
+}
+
+#commit1_legend_hash {
+ color: #28a745;
+}
+#commit2_legend_hash {
+ color: #28a745;
+}
+
+/* ==============================
+** ============================*/
+
+.hidden_iframe {
+ position:absolute; top:-1px; left:-1px; width:1px; height:1px;
+ display: none;
+}
+
+.shortcut_col {
+ width: 150px;
+ text-align: right;
+ padding-right: 15px;
+}
+
+.modal-content {
+ background: #333;
+}
+
+hr {
+ background-color: white;
+ height: 1px;
+ border: 0;
+}
diff --git a/kibot/resources/kiri/kiri.js b/kibot/resources/kiri/kiri.js
new file mode 100644
index 00000000..93d6f1c5
--- /dev/null
+++ b/kibot/resources/kiri/kiri.js
@@ -0,0 +1,1632 @@
+
+// jshint esversion:6
+
+var commit1;
+var commit2;
+
+var old_view;
+var current_view;
+
+current_diff_filter = "diff" // diff or normal
+
+var panZoom_instance = null;
+var lastEventListener = null;
+var lastEmbed = null;
+
+var current_selected_page = 0;
+var previous_selected_page = -1;
+
+sch_current_zoom = null;
+sch_old_zoom = null;
+sch_current_pan = null;
+
+pcb_current_zoom = null;
+pcb_old_zoom = null;
+pcb_current_pan = null;
+
+// Variables updated by Kiri
+var selected_view = "schematic";
+
+var is_fullscreen = false;
+
+// =======================================
+// HANDLE SHORTCUTS
+// =======================================
+
+function select_next_2_commits() {
+ commits = $("#commits_form input:checkbox[name='commit']");
+
+ selected_commits = [];
+ next_selected_commits = [];
+
+ for (i = 0; i < commits.length; i++) {
+ if ($("#commits_form input:checkbox[name='commit']")[i].checked) {
+ selected_commits.push(i);
+ next_selected_commits.push(i + 1);
+ }
+ }
+
+ // When second commit reaches the end, moves the first commit forward (if possible)
+ if (next_selected_commits[1] >= commits.length) {
+ next_selected_commits[1] = commits.length - 1;
+ if (next_selected_commits[0] <= commits.length - 2) {
+ next_selected_commits[0] = selected_commits[0] + 1;
+ }
+ }
+ else {
+ // By default does not change the first commit
+ next_selected_commits[0] = selected_commits[0];
+ }
+
+ // Fix bottom boundary
+ if (next_selected_commits[0] >= next_selected_commits[1]) {
+ next_selected_commits[0] = next_selected_commits[1] - 1;
+ }
+
+ // Fix bottom boundary
+ if (next_selected_commits[0] >= commits.length - 2) {
+ next_selected_commits[0] = commits.length - 2;
+ }
+
+ // Update selected commits
+ for (i = 0; i < selected_commits.length; i++) {
+ commits[selected_commits[i]].checked = false;
+ }
+ for (i = 0; i < selected_commits.length; i++) {
+ commits[next_selected_commits[i]].checked = true;
+ }
+
+ update_commits();
+}
+
+function select_next_commit() {
+ commits = $("#commits_form input:checkbox[name='commit']");
+
+ selected_commits = [];
+ next_selected_commits = [];
+
+ for (i = 0; i < commits.length; i++) {
+ if ($("#commits_form input:checkbox[name='commit']")[i].checked) {
+ selected_commits.push(i);
+ next_selected_commits.push(i + 1);
+ }
+ }
+
+ // Fix bottom boundary
+ if (next_selected_commits[1] >= commits.length - 1) {
+ next_selected_commits[1] = commits.length - 1;
+ }
+
+ // Fix bottom boundary
+ if (next_selected_commits[0] >= commits.length - 2) {
+ next_selected_commits[0] = commits.length - 2;
+ }
+
+ for (i = 0; i < selected_commits.length; i++) {
+ commits[selected_commits[i]].checked = false;
+ }
+ for (i = 0; i < selected_commits.length; i++) {
+ commits[next_selected_commits[i]].checked = true;
+ }
+
+ update_commits();
+}
+
+function select_previows_2_commits() {
+ commits = $("#commits_form input:checkbox[name='commit']");
+
+ selected_commits = [];
+ next_selected_commits = [];
+
+ for (i = 0; i < commits.length; i++) {
+ if ($("#commits_form input:checkbox[name='commit']")[i].checked) {
+ selected_commits.push(i);
+ next_selected_commits.push(i - 1);
+ }
+ }
+
+ // By default does not change the first commit
+ next_selected_commits[0] = selected_commits[0];
+
+ // When commits are touching, move first backwards (if possible)
+ if (next_selected_commits[1] == next_selected_commits[0]) {
+ if (next_selected_commits[0] > 0) {
+ next_selected_commits[0] = next_selected_commits[0] -1;
+ }
+ }
+
+ // Fix top boundary
+ if (next_selected_commits[0] < 0) {
+ next_selected_commits[0] = 0;
+ }
+
+ // Fix top boundary
+ if (next_selected_commits[1] <= 1) {
+ next_selected_commits[1] = 1;
+ }
+
+ // Update selected commits
+ for (i = 0; i < selected_commits.length; i++) {
+ commits[selected_commits[i]].checked = false;
+ }
+ for (i = 0; i < selected_commits.length; i++) {
+ commits[next_selected_commits[i]].checked = true;
+ }
+
+ update_commits();
+}
+
+function select_previows_commit()
+{
+ commits = $("#commits_form input:checkbox[name='commit']");
+
+ selected_commits = [];
+ next_selected_commits = [];
+
+ for (i = 0; i < commits.length; i++) {
+ if ($("#commits_form input:checkbox[name='commit']")[i].checked) {
+ selected_commits.push(i);
+ next_selected_commits.push(i - 1);
+ }
+ }
+
+ // Fix top boundary
+ if (next_selected_commits[0] <= 0) {
+ next_selected_commits[0] = 0;
+ }
+
+ // Fix top boundary
+ if (next_selected_commits[1] <= 1) {
+ next_selected_commits[1] = 1;
+ }
+
+ // Update selected commits
+ for (i = 0; i < selected_commits.length; i++) {
+ commits[selected_commits[i]].checked = false;
+ }
+ for (i = 0; i < selected_commits.length; i++) {
+ commits[next_selected_commits[i]].checked = true;
+ }
+
+ update_commits();
+}
+
+function reset_commits_selection()
+{
+ commits = $("#commits_form input:checkbox[name='commit']");
+ selected_commits = [];
+ for (i = 0; i < commits.length; i++) {
+ $("#commits_form input:checkbox[name='commit']")[i].checked = false;
+ }
+ for (i = 0; i < 2; i++) {
+ $("#commits_form input:checkbox[name='commit']")[i].checked = true;
+ }
+
+ // reset visibility of the diff images
+ $("#diff-xlink-1").css('visibility', 'visible')
+ $("#commit1_legend").css('visibility', 'visible');
+ $("#commit1_legend_text").css('visibility', 'visible');
+ $("#commit1_legend_fs").css('visibility', 'visible');
+ $("#commit1_legend_text_fs").css('visibility', 'visible');
+ $("#commit1_legend").css('color', '#00FFFF');
+ $("#commit1_legend_fs").css('color', '#00FFFF');
+
+ $("#diff-xlink-2").css('visibility', 'visible')
+ $("#commit2_legend").css('visibility', 'visible');
+ $("#commit2_legend_text").css('visibility', 'visible');
+ $("#commit2_legend_fs").css('visibility', 'visible');
+ $("#commit2_legend_text_fs").css('visibility', 'visible');
+ $("#commit2_legend").css('color', '#880808');
+ $("#commit2_legend_fs").css('color', '#880808');
+
+ $("#commit3_legend").css('visibility', 'visible');
+ $("#commit3_legend_text").css('visibility', 'visible');
+ $("#commit3_legend_fs").css('visibility', 'visible');
+ $("#commit3_legend_text_fs").css('visibility', 'visible');
+
+ $("#diff-xlink-1").css('filter', 'url(#filter-1)') /// FILTER_DEFAULT
+ $("#diff-xlink-2").css('filter', 'url(#filter-2)') /// FILTER_DEFAULT
+
+ update_commits();
+}
+
+function toggle_sch_pcb_view() {
+ old_view = current_view;
+ current_view = $('#view_mode input[name="view_mode"]:checked').val();
+ if (current_view == "show_sch") {
+ show_pcb();
+ } else {
+ show_sch();
+ }
+ update_commits();
+}
+
+function toggle_old_commit_visibility()
+{
+ if ($("#diff-xlink-1").css('visibility') === "hidden")
+ {
+ current_diff_filter = "diff";
+ $("#diff-xlink-1").css('visibility', 'visible')
+ $("#commit1_legend").css('visibility', 'visible');
+ $("#commit1_legend_text").css('visibility', 'visible');
+ $("#commit1_legend_fs").css('visibility', 'visible');
+ $("#commit1_legend_text_fs").css('visibility', 'visible');
+
+ $("#commit3_legend").css('visibility', 'visible');
+ $("#commit3_legend_text").css('visibility', 'visible');
+ $("#commit3_legend_fs").css('visibility', 'visible');
+ $("#commit3_legend_text_fs").css('visibility', 'visible');
+ }
+ else
+ {
+ current_diff_filter = "single";
+ $("#diff-xlink-1").css('visibility', 'hidden')
+ $("#commit1_legend").css('visibility', 'hidden');
+ $("#commit1_legend_text").css('visibility', 'hidden');
+ $("#commit1_legend_fs").css('visibility', 'hidden');
+ $("#commit1_legend_text_fs").css('visibility', 'hidden');
+
+ $("#commit3_legend").css('visibility', 'hidden');
+ $("#commit3_legend_text").css('visibility', 'hidden');
+ $("#commit3_legend_fs").css('visibility', 'hidden');
+ $("#commit3_legend_text_fs").css('visibility', 'hidden');
+ }
+
+ // enable the other image back
+ if ($("#diff-xlink-1").css('visibility') === "hidden")
+ {
+ $("#diff-xlink-2").css('visibility', 'visible')
+ $("#diff-xlink-2").css('filter', 'url(#filter-22)') /// FILTER_WHITE
+ $("#commit2_legend").css('visibility', 'visible');
+ $("#commit2_legend_text").css('visibility', 'visible');
+ $("#commit2_legend_fs").css('visibility', 'visible');
+ $("#commit2_legend_text_fs").css('visibility', 'visible');
+
+ $("#commit2_legend").css('color', '#a7a7a7');
+ $("#commit2_legend_fs").css('color', '#a7a7a7');
+ }
+ else
+ {
+ $("#diff-xlink-1").css('filter', 'url(#filter-1)') /// FILTER_DEFAULT
+ $("#diff-xlink-2").css('filter', 'url(#filter-2)') /// FILTER_DEFAULT
+
+ $("#commit1_legend").css('color', '#00FFFF');
+ $("#commit1_legend_fs").css('color', '#00FFFF');
+ $("#commit2_legend").css('color', '#880808');
+ $("#commit2_legend_fs").css('color', '#880808');
+ }
+}
+
+function toggle_new_commit_visibility()
+{
+ if ($("#diff-xlink-2").css('visibility') === "hidden")
+ {
+ current_diff_filter = "diff";
+ $("#diff-xlink-2").css('visibility', 'visible')
+ $("#commit2_legend").css('visibility', 'visible');
+ $("#commit2_legend_text").css('visibility', 'visible');
+ $("#commit2_legend_fs").css('visibility', 'visible');
+ $("#commit2_legend_text_fs").css('visibility', 'visible');
+
+ $("#commit3_legend").css('visibility', 'visible');
+ $("#commit3_legend_text").css('visibility', 'visible');
+ $("#commit3_legend_fs").css('visibility', 'visible');
+ $("#commit3_legend_text_fs").css('visibility', 'visible');
+ }
+ else
+ {
+ current_diff_filter = "single";
+ $("#diff-xlink-2").css('visibility', 'hidden')
+ $("#commit2_legend").css('visibility', 'hidden');
+ $("#commit2_legend_text").css('visibility', 'hidden');
+ $("#commit2_legend_fs").css('visibility', 'hidden');
+ $("#commit2_legend_text_fs").css('visibility', 'hidden');
+
+ $("#commit3_legend").css('visibility', 'hidden');
+ $("#commit3_legend_text").css('visibility', 'hidden');
+ $("#commit3_legend_fs").css('visibility', 'hidden');
+ $("#commit3_legend_text_fs").css('visibility', 'hidden');
+ }
+
+ // enable the other image back
+ if ($("#diff-xlink-2").css('visibility') === "hidden")
+ {
+ $("#diff-xlink-1").css('visibility', 'visible')
+ $("#diff-xlink-1").css('filter', 'url(#filter-12)') /// FILTER_WHITE
+ $("#commit1_legend").css('visibility', 'visible');
+ $("#commit1_legend_text").css('visibility', 'visible');
+ $("#commit1_legend_fs").css('visibility', 'visible');
+ $("#commit1_legend_text_fs").css('visibility', 'visible');
+
+ $("#commit1_legend").css('color', '#a7a7a7');
+ $("#commit1_legend_text_fs").css('color', '#a7a7a7');
+ }
+ else
+ {
+ $("#diff-xlink-1").css('filter', 'url(#filter-1)') /// FILTER_DEFAULT
+ $("#diff-xlink-2").css('filter', 'url(#filter-2)') /// FILTER_DEFAULT
+
+ $("#commit1_legend").css('color', '#00FFFF');
+ $("#commit1_legend_fs").css('color', '#00FFFF');
+ $("#commit2_legend").css('color', '#880808');
+ $("#commit2_legend_fs").css('color', '#880808');
+ }
+}
+
+function select_next_sch_or_pcb(cycle = false) {
+ if (document.getElementById("show_sch").checked) {
+ pages = $("#pages_list input:radio[name='pages']");
+ selected_page = pages.index(pages.filter(':checked'));
+
+ new_index = selected_page + 1;
+ if (new_index >= pages.length) {
+ if (cycle) {
+ new_index = 0;
+ }
+ else {
+ new_index = pages.length - 1;
+ }
+ }
+
+ pages[new_index].checked = true;
+
+ update_page();
+ }
+ else
+ {
+ layers = $("#layers_list input:radio[name='layers']");
+ selected_layer = layers.index(layers.filter(':checked'));
+
+ new_index = selected_layer + 1;
+ if (new_index >= layers.length) {
+ if (cycle) {
+ new_index = 0;
+ }
+ else {
+ new_index = layers.length - 1;
+ }
+ }
+
+ layers[new_index].checked = true;
+
+ update_layer();
+ }
+}
+
+function select_preview_sch_or_pcb(cycle = false) {
+ if (document.getElementById("show_sch").checked) {
+ pages = $("#pages_list input:radio[name='pages']");
+ selected_page = pages.index(pages.filter(':checked'));
+
+ new_index = selected_page - 1;
+ if (new_index < 0) {
+ if (cycle) {
+ new_index = pages.length - 1;
+ }
+ else {
+ new_index = 0;
+ }
+ }
+
+ pages[new_index].checked = true;
+
+ update_page();
+ update_sheets_list(commit1, commit2);
+
+ } else {
+ layers = $("#layers_list input:radio[name='layers']");
+ selected_layer = layers.index(layers.filter(':checked'));
+
+ new_index = selected_layer - 1;
+ if (new_index < 0) {
+ if (cycle) {
+ new_index = layers.length - 1;
+ }
+ else {
+ new_index = 0;
+ }
+ }
+
+ layers[new_index].checked = true;
+
+ update_layer();
+ }
+}
+
+function svg_fit_center()
+{
+ panZoom_instance.resetZoom();
+ panZoom_instance.center();
+}
+
+function svg_zoom_in()
+{
+ panZoom_instance.zoomIn();
+}
+
+function svg_zoom_out()
+{
+ panZoom_instance.zoomOut();
+}
+
+function manual_pan(direction)
+{
+ const step = 50;
+
+ switch(direction) {
+ case "up":
+ panZoom_instance.panBy({x: 0, y: step});
+ break;
+ case "down":
+ panZoom_instance.panBy({x: 0, y: -step});
+ break;
+ case "left":
+ panZoom_instance.panBy({x: step, y: 0});
+ break;
+ case "right":
+ panZoom_instance.panBy({x: -step, y: 0});
+ break;
+ }
+}
+
+// Commits
+Mousetrap.bind(['ctrl+down', 'ctrl+]','command+down', 'command+]'], function(){select_next_2_commits()});
+Mousetrap.bind(['down', ']'], function(){select_next_commit()});
+
+Mousetrap.bind(['ctrl+up', 'ctrl+[', 'command+up', 'command+['], function(){select_previows_2_commits()});
+Mousetrap.bind(['up', '['], function(){select_previows_commit()});
+
+Mousetrap.bind(['r', 'R'], function(){reset_commits_selection()});
+
+// View
+Mousetrap.bind(['s', 'S'], function(){toggle_sch_pcb_view()});
+
+Mousetrap.bind(['q', 'Q'], function(){toggle_old_commit_visibility()});
+Mousetrap.bind(['w', 'W'], function(){toggle_new_commit_visibility()});
+
+Mousetrap.bind(['alt+q', 'alt+Q'], function(){toggle_new_commit_visibility()});
+Mousetrap.bind(['alt+w', 'alt+W'], function(){toggle_old_commit_visibility()});
+
+Mousetrap.bind(['right'], function(){select_next_sch_or_pcb()});
+Mousetrap.bind(['left'], function(){select_preview_sch_or_pcb()});
+
+Mousetrap.bind(['ctrl+right', 'command+right'], function(){select_next_sch_or_pcb(true)});
+Mousetrap.bind(['ctrl+left', 'command+left'], function(){select_preview_sch_or_pcb(true)});
+
+// SVG PAN
+Mousetrap.bind('alt+up', function(){manual_pan("up")});
+Mousetrap.bind('alt+down', function(){manual_pan("down")});
+Mousetrap.bind('alt+left', function(){manual_pan("left")});
+Mousetrap.bind('alt+right', function(){manual_pan("right")});
+
+// SVG ZOOM
+Mousetrap.bind('0', function(){svg_fit_center()});
+Mousetrap.bind(['+', '='], function(){svg_zoom_in()});
+Mousetrap.bind('-', function(){svg_zoom_out()});
+
+// Misc
+Mousetrap.bind(['f', 'F'], function(){toggle_fullscreen()});
+Mousetrap.bind(['i', 'I'], function(){show_info_popup()});
+
+// =======================================
+// =======================================
+
+// For images related with each commit, it is good to have the same image cached with the same specially when serving throug the internet
+// For those images, it uses the commit hash as the timestamp
+function url_timestamp(timestamp_id="") {
+ if (timestamp_id) {
+ return "?t=" + timestamp_id;
+ }
+ else {
+ return "?t=" + new Date().getTime();
+ }
+}
+
+function if_url_exists(url, callback) {
+ let request = new XMLHttpRequest();
+ request.open('GET', url, true);
+ request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
+ request.setRequestHeader('Accept', '*/*');
+ request.onprogress = function(event) {
+ let status = event.target.status;
+ let statusFirstNumber = (status).toString()[0];
+ switch (statusFirstNumber) {
+ case '2':
+ request.abort();
+ return callback(true);
+ default:
+ request.abort();
+ return callback(false);
+ }
+ };
+ request.send('');
+}
+
+function update_commits() {
+
+ // Remove tooltips so they dont get stuck
+ $('[data-toggle="tooltip"]').tooltip("hide");
+
+ console.log("================================================================================");
+
+ var commits = $("#commits_form input:checkbox[name='commit']");
+ var hashes = [];
+
+ for (var i = 0; i < commits.length; i++) {
+ if (commits[i].checked) {
+ var value = commits[i].value;
+ hashes.push(value);
+ }
+ }
+
+ // It needs 2 items selected to do something
+ if (hashes.length < 2) {
+ return;
+ }
+
+ // Update selected commits
+ commit1 = hashes[0].replace(/\s+/g, '');
+ commit2 = hashes[1].replace(/\s+/g, '');
+
+ console.log("commit1:", commit1);
+ console.log("commit2:", commit2);
+
+
+ // 1. Update commit_legend_links
+ // 2. Update commit_legend
+ // 3. Update current_diff_view
+
+
+ // Update commit_legend_links
+
+ var old_commit1 = document.getElementById("commit1_hash").value;
+ var old_commit2 = document.getElementById("commit2_hash").value;
+
+ var kicad_pro_path_1 = document.getElementById("commit1_kicad_pro_path").value;
+ var kicad_pro_path_2 = document.getElementById("commit2_kicad_pro_path").value;
+
+ document.getElementById("commit1_kicad_pro_path").value = kicad_pro_path_1.replace(old_commit1, commit1);
+ document.getElementById("commit2_kicad_pro_path").value = kicad_pro_path_2.replace(old_commit2, commit2);
+
+ // Update commit_legend
+
+ document.getElementById("commit1_hash").value = commit1;
+ document.getElementById("commit2_hash").value = commit2;
+
+ document.getElementById("commit1_legend_hash").innerHTML = commit1;
+ document.getElementById("commit2_legend_hash").innerHTML = commit2;
+
+ // Update current_diff_view
+
+ old_view = current_view;
+ current_view = $('#view_mode input[name="view_mode"]:checked').val();
+
+ if (current_view == "show_sch") {
+ update_page();
+ } else {
+ update_layer();
+ }
+}
+
+function loadFile(filePath) {
+
+ console.log("filePath:", filePath);
+
+ var result = null;
+ var xmlhttp = new XMLHttpRequest();
+ xmlhttp.open('GET', filePath, false);
+ xmlhttp.send();
+ if (xmlhttp.status==200) {
+ result = xmlhttp.responseText;
+ }
+ return result;
+}
+
+function update_page()
+{
+ console.log("-----------------------------------------");
+
+ // Runs only when updating commits
+ update_sheets_list(commit1, commit2);
+
+ var pages = $("#pages_list input:radio[name='pages']");
+ var selected_page;
+ var page_name;
+
+ // if a different page was in use before, revert the selection to it
+ // TODO: maybe I have to use a list instead...
+ if (previous_selected_page > -1) {
+ pages[previous_selected_page].checked = true;
+ previous_selected_page = -1;
+ }
+
+ // try to get the first page
+ try {
+ selected_page = pages.index(pages.filter(':checked'));
+ page_name = pages[selected_page].id;
+ current_selected_page = selected_page;
+
+ // if there is no page selected, select the first one
+ // TODO: instead of the first item by default, a better solution would change to the next inferior index
+ // and keep decrementing until reaching a valid index
+ } catch (error) {
+ previous_selected_page = current_selected_page;
+ pages[0].checked = true;
+ selected_page = pages.index(pages.filter(':checked'));
+ page_name = pages[selected_page].id;
+ }
+
+ var page_filename = pages[selected_page].value.replace(".kicad_sch", "").replace(".sch", "");
+
+ if (commit1 == ""){
+ commit1 = document.getElementById("diff-xlink-1-sch").href.baseVal.split("/")[1];
+ }
+ if (commit2 == ""){
+ commit2 = document.getElementById("diff-xlink-2-sch").href.baseVal.split("/")[1];
+ }
+
+ var image_path_1 = "../" + commit1 + "/_KIRI_/sch/" + page_filename + ".svg";
+ var image_path_2 = "../" + commit2 + "/_KIRI_/sch/" + page_filename + ".svg";
+
+ console.log("[SCH] page_filename =", page_filename);
+ console.log("[SCH] image_path_1 =", image_path_1);
+ console.log("[SCH] image_path_2 =", image_path_2);
+
+ var image_path_timestamp_1 = image_path_1 + url_timestamp(commit1);
+ var image_path_timestamp_2 = image_path_2 + url_timestamp(commit2);
+
+ if (current_view != old_view)
+ {
+ old_view = current_view;
+ removeEmbed();
+ lastEmbed = createNewEmbed(image_path_timestamp_1, image_path_timestamp_2);
+ }
+ else
+ {
+ document.getElementById("diff-xlink-1").href.baseVal = image_path_timestamp_1;
+ document.getElementById("diff-xlink-2").href.baseVal = image_path_timestamp_2;
+
+ document.getElementById("diff-xlink-1").setAttributeNS('http://www.w3.org/1999/xlink', 'href', image_path_timestamp_1);
+ document.getElementById("diff-xlink-2").setAttributeNS('http://www.w3.org/1999/xlink', 'href', image_path_timestamp_2);
+
+ if_url_exists(image_path_timestamp_1, function(exists) {
+ if (exists == true) {
+ document.getElementById("diff-xlink-1").parentElement.style.display = 'inline' }
+ else {
+ document.getElementById("diff-xlink-1").parentElement.style.display = "none";
+ }
+ });
+
+ if_url_exists(image_path_timestamp_2, function(exists) {
+ if (exists == true) {
+ document.getElementById("diff-xlink-2").parentElement.style.display = 'inline';
+ }
+ else {
+ document.getElementById("diff-xlink-2").parentElement.style.display = "none";
+ }
+ });
+ }
+
+ // keep images visibility the same as the legend
+ $("#diff-xlink-1").css('visibility', $("#commit1_legend").css('visibility'))
+ $("#diff-xlink-2").css('visibility', $("#commit2_legend").css('visibility'))
+
+ update_fullscreen_label();
+}
+
+function update_sheets_list(commit1, commit2) {
+
+ // Get current selected page name
+ var pages = $("#pages_list input:radio[name='pages']");
+ var selected_page = pages.index(pages.filter(':checked'));
+
+ // Save the current selected page, if any
+ try {
+ selected_sheet = pages[selected_page].id;
+ }
+ catch(err) {
+ selected_page = "";
+ console.log("There isn't a sheet selected");
+ }
+
+ // Data format: ID|LAYER
+
+ data1 = loadFile("../" + commit1 + "/_KIRI_/sch_sheets" + url_timestamp(commit1)).split("\n").filter((a) => a);
+ data2 = loadFile("../" + commit2 + "/_KIRI_/sch_sheets" + url_timestamp(commit2)).split("\n").filter((a) => a);
+
+ var sheets = [];
+
+ for (const d of data1)
+ {
+ sheet = d.split("|")[0];
+ sheets.push(sheet);
+ }
+
+ for (const d of data2)
+ {
+ sheet = d.split("|")[0];
+ if (! sheets.includes(sheet))
+ {
+ sheets.push(sheet);
+ }
+ }
+
+ // sheets.sort();
+ // sheets = Array.from(new Set(sheets.sort()));
+ sheets = Array.from(new Set(sheets));
+
+ console.log("[SCH] Sheets =", sheets.length);
+ console.log("sheets", sheets);
+
+ var new_sheets_list = [];
+ var form_inputs_html;
+
+ for (const sheet of sheets)
+ {
+ var input_html = `
+
+
+
+ `;
+
+ new_sheets_list.push(sheet);
+
+ form_inputs_html = form_inputs_html + input_html;
+ }
+
+ // Get the current list of pages
+ pages = $("#pages_list input:radio[name='pages']");
+ const current_sheets_list = Array.from(pages).map((opt) => opt.id);
+
+ // Return if the current list is equal to the new list
+ console.log("current_sheets_list = ", current_sheets_list);
+ console.log("new_sheets_list = ", new_sheets_list);
+ if (current_sheets_list.toString() === new_sheets_list.toString()) {
+ console.log("Keep the same list of sheets");
+ return;
+ }
+
+ // Update list of pages
+ sheets_element = document.getElementById("pages_list_form");
+ sheets_element.innerHTML = form_inputs_html.replace("undefined", "");
+
+ // rerun tooltips since they are getting ugly.
+ $('[data-toggle="tooltip"]').tooltip({html: true});
+ $('[data-toggle="tooltip"]').tooltip('update');
+ $('[data-toggle="tooltip"]').tooltip({boundary: 'body'});
+
+ const optionLabels = Array.from(pages).map((opt) => opt.id);
+
+ const hasOption = optionLabels.includes(selected_sheet);
+ if (hasOption) {
+ // Keep previews selection active
+ $("#pages_list input:radio[name='pages'][value='" + selected_sheet + "']").prop('checked', true);
+ }
+ else {
+ // If old selection does not exist, maybe the list is now shorter, then select the last item...
+ pages[optionLabels.length-1].checked = true;
+ }
+
+ // If nothing is selected still, select the first item
+ if (!pages.filter(':checked').length) {
+ pages[0].checked = true;
+ }
+}
+
+function layer_color(layer_id) {
+
+ var color;
+
+ console.log(">>> layer_id", layer_id);
+
+ const F_Cu = 0;
+ const In1_Cu = 1;
+ const In2_Cu = 2;
+ const In3_Cu = 3;
+ const In4_Cu = 4;
+ const B_Cu = 31;
+ const B_Adhes = 32;
+ const F_Adhes = 33;
+ const B_Paste = 34;
+ const F_Paste = 35;
+ const B_SilkS = 36;
+ const F_SilkS = 37;
+ const B_Mask = 38;
+ const F_Mask = 39;
+ const Dwgs_User = 40;
+ const Cmts_User = 41;
+ const Eco1_User = 42;
+ const Eco2_User = 43;
+ const Edge_Cuts = 44;
+ const Margin = 45;
+ const B_CrtYd = 46;
+ const F_CrtYd = 47;
+ const B_Fab = 48;
+ const F_Fab = 49;
+
+ switch(layer_id) {
+ case B_Adhes: color="#3545A8"; break;
+ case B_CrtYd: color="#D3D04B"; break;
+ case B_Cu: color="#359632"; break;
+ case B_Fab: color="#858585"; break;
+ case B_Mask: color="#943197"; break;
+ case B_Paste: color="#969696"; break;
+ case B_SilkS: color="#481649"; break;
+ case Cmts_User: color="#7AC0F4"; break;
+ case Dwgs_User: color="#0364D3"; break;
+ case Eco1_User: color="#008500"; break;
+ case Eco2_User: color="#008500"; break;
+ case Edge_Cuts: color="#C9C83B"; break;
+ case F_Adhes: color="#A74AA8"; break;
+ case F_CrtYd: color="#A7A7A7"; break;
+ case F_Cu: color="#952927"; break;
+ case F_Fab: color="#C2C200"; break;
+ case F_Mask: color="#943197"; break;
+ case F_Paste: color="#3DC9C9"; break;
+ case F_SilkS: color="#339697"; break;
+ case In1_Cu: color="#C2C200"; break;
+ case In2_Cu: color="#C200C2"; break;
+ case In3_Cu: color="#C20000"; break;
+ case In4_Cu: color="#0000C2"; break;
+ case Margin: color="#D357D2"; break;
+ default: color="#DBDBDB";
+ }
+
+ return color;
+}
+
+function pad(num, size)
+{
+ num = num.toString();
+ while (num.length < size) {
+ num = "0" + num;
+ }
+ return num;
+}
+
+function update_layers_list(commit1, commit2, selected_layer_idx, selected_layer_id)
+{
+ var used_layers_1;
+ var used_layers_2;
+
+ var id;
+ var layer;
+ var dict = {};
+
+ var id_pad;
+ var layer_name;
+ var color;
+ var checked;
+
+ var new_layers_list = [];
+ var form_inputs_html;
+
+ // Get current selected page name
+ var layers = $("#layers_list input:radio[name='layers']");
+ var selected_layer_element = layers.index(layers.filter(':checked'));
+
+ // Save the current selected page, if any
+ try {
+ selected_layer = layers[selected_layer_element].id;
+ }
+ catch(err) {
+ selected_layer = "";
+ console.log("There isn't a layer selected");
+ }
+
+ // File = ../[COMMIT]/_KIRI_/pcb_layers
+ // Format = ID|LAYER
+
+ used_layers_1 = loadFile("../" + commit1 + "/_KIRI_/pcb_layers" + url_timestamp(commit1)).split("\n").filter((a) => a);
+ used_layers_2 = loadFile("../" + commit2 + "/_KIRI_/pcb_layers" + url_timestamp(commit2)).split("\n").filter((a) => a);
+
+ for (const line of used_layers_1)
+ {
+ id = line.split("|")[0];
+ layer = line.split("|")[1]; //.replace(".", "_");
+ dict[id] = [layer];
+ }
+
+ for (const line of used_layers_2)
+ {
+ id = line.split("|")[0];
+ layer = line.split("|")[1]; //.replace(".", "_");
+
+ // Add new key
+ if (! dict.hasOwnProperty(id)) {
+ dict[id] = [layer];
+ }
+ else {
+ // Append if id key exists
+ if (dict[id] != layer) {
+ dict[id].push(layer);
+ }
+ }
+ }
+
+ console.log("[PCB] Layers =", Object.keys(dict).length);
+
+ for (const [layer_id, layer_names] of Object.entries(dict))
+ {
+ id = parseInt(layer_id);
+ id_pad = pad(id, 2);
+ layer_name = layer_names[0];
+ color = layer_color(id);
+
+ var input_html = `
+
+
+
+ `;
+
+ new_layers_list.push(layer_names.toString());
+
+ form_inputs_html = form_inputs_html + input_html;
+ }
+
+ // Get the current list of pages
+ const current_layers_list = Array.from(layers).map((opt) => opt.value.replace("layer-", ""));
+
+ // Return if the current list is equal to the new list
+ console.log("current_layers_list = ", current_layers_list);
+ console.log("new_layers_list = ", new_layers_list);
+ if (current_layers_list.toString() === new_layers_list.toString()) {
+ console.log("Keep the same list of layers");
+ return;
+ }
+
+ // Update layers list
+ layers_element = document.getElementById("layers_list_form");
+ layers_element.innerHTML = form_inputs_html.replace("undefined", "");
+
+ // Update html tooltips
+ $('[data-toggle="tooltip"]').tooltip({html:true});
+ $('[data-toggle="tooltip"]').tooltip('update');
+ $('[data-toggle="tooltip"]').tooltip({boundary: 'body'});
+
+ // Enable back the selected layer
+ const optionLabels = Array.from(layers).map((opt) => opt.id);
+
+ const hasOption = optionLabels.includes(selected_layer);
+ if (hasOption) {
+ // Keep previews selection active
+ $("#layers_list input:radio[name='layers'][value=" + selected_layer + "]").prop('checked', true);
+ }
+ else {
+ // If old selection does not exist, maybe the list is now shorter, then select the last item...
+ layers[optionLabels.length-1].checked = true;
+ }
+
+ // restore previously selected index
+ layers = $("#layers_list input:radio[name='layers']");
+ if (selected_layer_idx >= 0) {
+ layers[selected_layer_idx].checked = true;
+ }
+
+ // If nothing is selected still, select the first item
+ if (! layers.filter(':checked').length) {
+ layers[0].checked = true;
+ }
+}
+
+function update_layer() {
+
+ console.log("-----------------------------------------");
+
+ var layers = $("#layers_list input:radio[name='layers']");
+ var selected_layer;
+ var layer_id;
+
+ if (layers)
+ {
+ selected_layer = layers.index(layers.filter(':checked'));
+ console.log(">>>> [selected_layer] = ", selected_layer);
+ if (selected_layer >= 0) {
+ layer_id = layers[selected_layer].id.split("-")[1];
+ console.log(">>>> [label_id_IF] = ", layer_id);
+ }
+ else {
+ try {
+ layers[0].checked = true;
+ selected_layer = layers.index(layers.filter(':checked'));
+ layer_id = layers[selected_layer].id.split("-")[1];
+ console.log(">>>> [label_id_ELSE] = ", layer_id);
+ } catch (error) {
+ console.log("[PCB] Images may not exist and Kicad layout may be missing.");
+ show_sch();
+ return;
+ }
+ }
+ }
+ else {
+ console.log("[PCB] Images may not exist and Kicad layout may be missing.");
+ show_sch();
+ return;
+ }
+
+ if (commit1 == "") {
+ commit1 = document.getElementById("diff-xlink-1-pcb").href.baseVal.split("/")[1];
+ }
+ if (commit2 == "") {
+ commit2 = document.getElementById("diff-xlink-2-pcb").href.baseVal.split("/")[1];
+ }
+
+ update_layers_list(commit1, commit2, selected_layer, layer_id);
+
+ var image_path_1 = "../" + commit1 + "/_KIRI_/pcb/layer" + "-" + layer_id + ".svg";
+ var image_path_2 = "../" + commit2 + "/_KIRI_/pcb/layer" + "-" + layer_id + ".svg";
+
+ console.log("[PCB] layer_id =", layer_id);
+ console.log("[PCB] image_path_1 =", image_path_1);
+ console.log("[PCB] image_path_2 =", image_path_2);
+
+ var image_path_timestamp_1 = image_path_1 + url_timestamp(commit1);
+ var image_path_timestamp_2 = image_path_2 + url_timestamp(commit2);
+
+ if (current_view != old_view)
+ {
+ old_view = current_view;
+ removeEmbed();
+ lastEmbed = createNewEmbed(image_path_timestamp_1, image_path_timestamp_2);
+ }
+ else
+ {
+ document.getElementById("diff-xlink-1").href.baseVal = image_path_timestamp_1;
+ document.getElementById("diff-xlink-2").href.baseVal = image_path_timestamp_2;
+
+ document.getElementById("diff-xlink-1").setAttributeNS('http://www.w3.org/1999/xlink', 'href', image_path_timestamp_1);
+ document.getElementById("diff-xlink-2").setAttributeNS('http://www.w3.org/1999/xlink', 'href', image_path_timestamp_2);
+
+ if_url_exists(image_path_timestamp_1, function(exists) {
+ if (exists == true) {
+ document.getElementById("diff-xlink-1").parentElement.style.display = 'inline' }
+ else {
+ document.getElementById("diff-xlink-1").parentElement.style.display = "none";
+ }
+ });
+
+ if_url_exists(image_path_timestamp_2, function(exists) {
+ if (exists == true) {
+ document.getElementById("diff-xlink-2").parentElement.style.display = 'inline';
+ }
+ else {
+ document.getElementById("diff-xlink-2").parentElement.style.display = "none";
+ }
+ });
+ }
+
+ // keep images visibility the same as the legend
+ $("#diff-xlink-1").css('visibility', $("#commit1_legend").css('visibility'))
+ $("#diff-xlink-2").css('visibility', $("#commit2_legend").css('visibility'))
+
+ update_fullscreen_label();
+}
+
+// =======================================
+// SVG Controls
+// =======================================
+
+function select_initial_commits()
+{
+ var commits = $("#commits_form input:checkbox[name='commit']");
+
+ if (commits.length >= 2)
+ {
+ commit1 = commits[0].value;
+ commit2 = commits[1].value;
+ commits[0].checked = true;
+ commits[1].checked = true;
+ }
+ else if (commits.length == 1)
+ {
+ commit1 = commits[0].value;
+ commits[0].checked = true;
+ }
+}
+
+function get_selected_commits()
+{
+ var commits = [];
+ var hashes = [];
+ for (var i = 0; i < commits.length; i++) {
+ if ($("#commits_form input:checkbox[name='commit']")[i].checked) {
+ var value = $("#commits_form input:checkbox[name='commit']")[i].value;
+ hashes.push(value);
+ }
+ }
+
+ // It needs 2 items selected to do something
+ if (hashes.length < 2) {
+ return;
+ }
+
+ var commit1 = hashes[0].replace(/\s+/g, '');
+ var commit2 = hashes[1].replace(/\s+/g, '');
+
+ return [commit1, commit2];
+}
+
+
+// Interpret tooltois as html
+$(document).ready(function()
+{
+ $('[data-toggle="tooltip"]').tooltip({html:true});
+ $('[data-toggle="tooltip"]').tooltip('update');
+ $('[data-toggle="tooltip"]').tooltip({boundary: 'body'});
+});
+
+// Limit commits list with 2 checked commits at most
+$(document).ready(function()
+{
+ $("#commits_form input:checkbox[name='commit']").change(function() {
+ var max_allowed = 2;
+ var count = $("input[name='commit']:checked").length;
+ if (count > max_allowed) {
+ $(this).prop("checked", "");
+ }
+ });
+});
+
+function ready()
+{
+ check_server_status();
+ select_initial_commits();
+
+ update_commits();
+
+ if (selected_view == "schematic") {
+ // show_sch();
+ update_page(commit1, commit2);
+ }
+ else {
+ // show_pcb();
+ update_layer(commit1, commit2);
+ }
+}
+
+window.onload = function()
+{
+ console.log("function onload");
+};
+
+window.addEventListener('DOMContentLoaded', ready);
+
+// =======================================
+// Toggle Schematic/Layout
+// =======================================
+
+function show_sch()
+{
+ // Show schematic stuff
+ document.getElementById("show_sch_lbl").classList.add('active');
+ document.getElementById("show_sch").checked = true;
+ // document.getElementById("diff-sch").style.display = "inline";
+ document.getElementById("diff-xlink-1").parentElement.style.display = "inline";
+ document.getElementById("diff-xlink-2").parentElement.style.display = "inline";
+ document.getElementById("pages_list").style.display = "inline";
+ document.getElementById("sch_title").style.display = "inline";
+
+ // Hide layout stuff
+ document.getElementById("show_pcb_lbl").classList.remove('active');
+ document.getElementById("show_pcb").checked = false;
+ // document.getElementById("diff-pcb").style.display = "none";
+ // document.getElementById("diff-xlink-1-pcb").parentElement.style.display = "none";
+ // document.getElementById("diff-xlink-2-pcb").parentElement.style.display = "none";
+ document.getElementById("layers_list").style.display = "none";
+ document.getElementById("pcb_title").style.display = "none";
+
+ update_page(commit1, commit2);
+}
+
+function show_pcb()
+{
+ // Show layout stuff
+ document.getElementById("show_pcb_lbl").classList.add('active');
+ document.getElementById("show_pcb").checked = true;
+ // document.getElementById("diff-pcb").style.display = "inline";
+ document.getElementById("diff-xlink-1").parentElement.style.display = "inline";
+ document.getElementById("diff-xlink-2").parentElement.style.display = "inline";
+ document.getElementById("layers_list").style.display = "inline";
+ document.getElementById("pcb_title").style.display = "inline";
+
+ // Hide schematic stuff
+ document.getElementById("show_sch_lbl").classList.remove('active');
+ document.getElementById("show_sch").checked = false;
+ // document.getElementById("diff-sch").style.display = "none";
+ // document.getElementById("diff-xlink-1-sch").parentElement.style.display = "none";
+ // document.getElementById("diff-xlink-2-sch").parentElement.style.display = "none";
+ document.getElementById("pages_list").style.display = "none";
+ document.getElementById("sch_title").style.display = "none";
+
+ update_layer(commit1, commit2);
+}
+
+// =======================================
+// Toggle Onion/Slide
+// =======================================
+
+function show_onion() {
+ // console.log("Function:", "show_onion");
+}
+
+function show_slide() {
+ // console.log("Function:", "show_slide");
+}
+
+// =======================================
+// =======================================
+
+function update_page_onclick(obj) {
+ update_page();
+}
+
+function update_layer_onclick(obj) {
+ update_layer();
+}
+
+// Hide fields with missing images
+function imgError(image)
+{
+ // console.log("Image Error (missing or problematic) =", image.href.baseVal);
+ image.onerror = null;
+ parent = document.getElementById(image.id).parentElement;
+ parent.style.display = "none";
+ return true;
+}
+
+
+// #===========================
+
+var server_status = 1;
+var old_server_status = -1;
+
+function check_server_status()
+{
+ var img;
+
+ img = document.getElementById("server_status_img");
+
+ if (! img) {
+ img = document.body.appendChild(document.createElement("img"));
+ img.setAttribute("id", "server_status_img");
+ img.style.display = "none";
+ }
+
+ img.onload = function() {
+ server_is_online();
+ };
+
+ img.onerror = function() {
+ server_is_offline();
+ };
+
+ img.src = "favicon.ico" + url_timestamp();
+
+ setTimeout(check_server_status, 5000);
+}
+
+function server_is_online() {
+ server_status = 1;
+ document.getElementById("server_offline").style.display = "none";
+ if (server_status != old_server_status) {
+ old_server_status = server_status;
+ console.log("Server is Online");
+ }
+}
+
+function server_is_offline() {
+ server_status = 0;
+ document.getElementById("server_offline").style.display = "block";
+ if (server_status != old_server_status) {
+ old_server_status = server_status;
+ console.log("Server is Offline");
+ }
+}
+
+// ==================================================================
+
+function createNewEmbed(src1, src2)
+{
+ console.log("createNewEmbed...");
+
+ var embed = document.createElement('div');
+ embed.setAttribute('id', "diff-container");
+ embed.setAttribute('class', "position-relative");
+ embed.setAttribute('style', "padding: 0px; height: 94%;");
+
+ // WORKING WITH FILTERS..
+ // https://fecolormatrix.com/
+
+ var svg_element = `
+
+ `;
+
+ document.getElementById('diff-container').replaceWith(embed);
+ document.getElementById('diff-container').innerHTML = svg_element;
+ console.log(">>> SVG: ", embed);
+
+ svgpanzoom_selector = "#svg-id";
+
+
+ panZoom_instance = svgPanZoom(
+ svgpanzoom_selector, {
+ zoomEnabled: true,
+ controlIconsEnabled: false,
+ center: true,
+ minZoom: 1,
+ maxZoom: 20,
+ zoomScaleSensitivity: 0.22,
+ contain: false,
+ fit: false, // cannot be used, bug? (this one must be here to change the default)
+ viewportSelector: '.my_svg-pan-zoom_viewport',
+ eventsListenerElement: document.querySelector(svgpanzoom_selector),
+ onUpdatedCTM: function() {
+ if (current_view == "show_sch") {
+ if (sch_current_zoom != sch_old_zoom) {
+ console.log(">> Restoring SCH pan and zoom");
+ panZoom_instance.zoom(sch_current_zoom);
+ panZoom_instance.pan(sch_current_pan);
+ sch_old_zoom = sch_current_zoom;
+ }
+ }
+ else {
+ if (pcb_current_zoom != pcb_old_zoom) {
+ console.log(">> Restoring PCB pan and zoom");
+ panZoom_instance.zoom(pcb_current_zoom);
+ panZoom_instance.pan(pcb_current_pan);
+ pcb_old_zoom = pcb_current_zoom;
+ }
+ }
+ }
+ });
+
+ console.log("panZoom_instance:", panZoom_instance);
+
+ embed.addEventListener('load', lastEventListener);
+
+ document.getElementById('zoom-in').addEventListener('click', function(ev) {
+ ev.preventDefault();
+ panZoom_instance.zoomIn();
+ panZoom_instance.center();
+ });
+
+ document.getElementById('zoom-out').addEventListener('click', function(ev) {
+ ev.preventDefault();
+ panZoom_instance.zoomOut();
+ panZoom_instance.center();
+ });
+
+ document.getElementById('zoom-fit').addEventListener('click', function(ev) {
+ ev.preventDefault();
+ panZoom_instance.resetZoom();
+ panZoom_instance.center();
+ });
+
+ if (current_diff_filter === "diff")
+ {
+ $("#diff-xlink-1").css('filter', 'url(#filter-1)') /// FILTER_DIFF
+ $("#diff-xlink-2").css('filter', 'url(#filter-2)') /// FILTER_DIFF
+ }
+ else
+ {
+ $("#diff-xlink-1").css('filter', 'url(#filter-12)') /// FILTER_WHITE
+ $("#diff-xlink-2").css('filter', 'url(#filter-22)') /// FILTER_WHITE
+ }
+
+ return embed;
+}
+
+function removeEmbed()
+{
+ console.log(">=============================================<");
+ console.log("removeEmbed...");
+ console.log(">> lastEmbed: ", lastEmbed);
+ console.log(">> panZoom_instance: ", panZoom_instance);
+
+ // Destroy svgpanzoom
+ if (panZoom_instance)
+ {
+ if (current_view == "show_pcb") {
+ sch_current_zoom = panZoom_instance.getZoom();
+ sch_current_pan = panZoom_instance.getPan();
+ sch_old_zoom = null;
+ } else {
+ pcb_current_zoom = panZoom_instance.getZoom();
+ pcb_current_pan = panZoom_instance.getPan();
+ pcb_old_zoom = null;
+ }
+
+ panZoom_instance.destroy();
+
+ // Remove event listener
+ lastEmbed.removeEventListener('load', lastEventListener);
+
+ // Null last event listener
+ lastEventListener = null;
+
+ // Remove embed element
+ // document.getElementById('diff-container').removeChild(lastEmbed);
+
+ // Null reference to embed
+ lastEmbed = null;
+ }
+}
+
+function update_fullscreen_label()
+{
+ fullscreen_label = document.getElementById("fullscreen_label");
+
+ commit1 = document.getElementById("commit1_hash").value;
+ commit2 = document.getElementById("commit2_hash").value;
+
+ if (current_view == "show_sch")
+ {
+ pages = $("#pages_list input:radio[name='pages']");
+ selected_page = pages.index(pages.filter(':checked'));
+ page_name = document.getElementById("label-" + pages[selected_page].id).innerHTML;
+ view_item = "Page " + page_name;
+ }
+ else
+ {
+ layers = $("#layers_list input:radio[name='layers']");
+ selected_layer = layers.index(layers.filter(':checked'));
+ layer_name = document.getElementById("label-" + layers[selected_layer].id).innerHTML;
+ view_item = "Layer " + layer_name;
+ }
+
+ if (is_fullscreen)
+ {
+ if (fullscreen_label)
+ {
+ document.getElementById("commit1_fs").innerHTML = `(${commit1})`;
+ document.getElementById("commit2_fs").innerHTML = `(${commit2})`;
+ document.getElementById("view_item_fs").innerHTML = view_item;
+ }
+ else
+ {
+ label = `
+
+
+
+ Newer
+ (${commit1})
+
+
+
+
+
+ Older
+ (${commit2})
+
+
+
+
+
+ Unchanged
+
+
+
+ |
+
+
+
+ ${view_item}
+
+
+ `
+
+ const element = $('#diff-container').get(0);
+ element.insertAdjacentHTML("afterbegin", label);
+
+ var visibility1 = $("#diff-xlink-1").css('visibility');
+ $("#commit1_legend_fs").css('visibility', visibility1)
+ $("#commit1_legend_text_fs").css('visibility', visibility1)
+
+ var visibility2 = $("#diff-xlink-2").css('visibility');
+ $("#commit2_legend_fs").css('visibility', visibility2)
+ $("#commit2_legend_text_fs").css('visibility', visibility2)
+ }
+ }
+}
+
+function toggle_fullscreen()
+{
+ if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement)
+ {
+ if (document.exitFullscreen) {
+ document.exitFullscreen();
+ } else if (document.mozCancelFullScreen) {
+ document.mozCancelFullScreen();
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen();
+ } else if (document.msExitFullscreen) {
+ document.msExitFullscreen();
+ }
+
+ is_fullscreen = false;
+ const box = document.getElementById('fullscreen_label');
+ box.remove();
+
+ } else {
+ element = $('#diff-container').get(0);
+ if (element.requestFullscreen) {
+ element.requestFullscreen();
+ } else if (element.mozRequestFullScreen) {
+ element.mozRequestFullScreen();
+ } else if (element.webkitRequestFullscreen) {
+ element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
+ } else if (element.msRequestFullscreen) {
+ element.msRequestFullscreen();
+ }
+
+ is_fullscreen = true;
+ update_fullscreen_label()
+ }
+}
+
+function show_info_popup()
+{
+ document.getElementById("info-btn").click();
+}
+
+// Remove focus whne info buttons is clicked with shortcut i
+$('#shortcuts-modal').on('shown.bs.modal', function(e){
+ $('#info-btn').one('focus', function(e){$(this).blur();});
+});
+
+function change_page()
+{
+ update_page();
+}
diff --git a/kibot/resources/kiri/layers_template.html b/kibot/resources/kiri/layers_template.html
new file mode 100644
index 00000000..7aab62c7
--- /dev/null
+++ b/kibot/resources/kiri/layers_template.html
@@ -0,0 +1,6 @@
+
+
+
diff --git a/kibot/resources/kiri/pages_template.html b/kibot/resources/kiri/pages_template.html
new file mode 100644
index 00000000..99056165
--- /dev/null
+++ b/kibot/resources/kiri/pages_template.html
@@ -0,0 +1,6 @@
+
+
+
diff --git a/kibot/resources/kiri/redirect.html b/kibot/resources/kiri/redirect.html
new file mode 100644
index 00000000..722adbe5
--- /dev/null
+++ b/kibot/resources/kiri/redirect.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+