Files
DHO800_BatteryPack/2.BOM/DHO800_Battery_Pack_USB_C_V1.1.html
2023-09-02 18:26:36 +08:00

4461 lines
183 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive BOM for Altium Designer</title>
<style type="text/css">
:root {
--pcb-edge-color: black;
--pad-color: #878787;
--pad-hole-color: #CCCCCC;
--pad-color-highlight: #D04040;
--pad-color-highlight-both: #D0D040;
--pad-color-highlight-marked: #44a344;
--pin1-outline-color: #ffb629;
--pin1-outline-color-highlight: #ffb629;
--pin1-outline-color-highlight-both: #fcbb39;
--pin1-outline-color-highlight-marked: #fdbe41;
--silkscreen-edge-color: #aa4;
--silkscreen-polygon-color: #4aa;
--silkscreen-text-color: #4aa;
--fabrication-edge-color: #907651;
--fabrication-polygon-color: #907651;
--fabrication-text-color: #a27c24;
--track-color: #def5f1;
--track-color-highlight: #D04040;
--zone-color: #def5f1;
--zone-color-highlight: #d0404080;
}
html,
body {
margin: 0px;
height: 100%;
font-family: Verdana, sans-serif;
}
.dark.topmostdiv {
--pcb-edge-color: #eee;
--pad-color: #808080;
--pin1-outline-color: #ffa800;
--pin1-outline-color-highlight: #ccff00;
--track-color: #42524f;
--zone-color: #42524f;
background-color: #252c30;
color: #eee;
}
button {
background-color: #eee;
border: 1px solid #888;
color: black;
height: 44px;
width: 44px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
font-weight: bolder;
}
.dark button {
/* This will be inverted */
background-color: #c3b7b5;
}
button.depressed {
background-color: #0a0;
color: white;
}
.dark button.depressed {
/* This will be inverted */
background-color: #b3b;
}
button:focus {
outline: 0;
}
button#tb-btn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8.47 8.47'%3E%3Crect transform='translate(0 -288.53)' ry='1.17' y='288.8' x='.27' height='7.94' width='7.94' fill='%23f9f9f9'/%3E%3Cg transform='translate(0 -288.53)'%3E%3Crect width='7.94' height='7.94' x='.27' y='288.8' ry='1.17' fill='none' stroke='%23000' stroke-width='.4' stroke-linejoin='round'/%3E%3Cpath d='M1.32 290.12h5.82M1.32 291.45h5.82' fill='none' stroke='%23000' stroke-width='.4'/%3E%3Cpath d='M4.37 292.5v4.23M.26 292.63H8.2' fill='none' stroke='%23000' stroke-width='.3'/%3E%3Ctext font-weight='700' font-size='3.17' font-family='sans-serif'%3E%3Ctspan x='1.35' y='295.73'%3EF%3C/tspan%3E%3Ctspan x='5.03' y='295.68'%3EB%3C/tspan%3E%3C/text%3E%3C/g%3E%3C/svg%3E%0A");
}
button#lr-btn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8.47 8.47'%3E%3Crect transform='translate(0 -288.53)' ry='1.17' y='288.8' x='.27' height='7.94' width='7.94' fill='%23f9f9f9'/%3E%3Cg transform='translate(0 -288.53)'%3E%3Crect width='7.94' height='7.94' x='.27' y='288.8' ry='1.17' fill='none' stroke='%23000' stroke-width='.4' stroke-linejoin='round'/%3E%3Cpath d='M1.06 290.12H3.7m-2.64 1.33H3.7m-2.64 1.32H3.7m-2.64 1.3H3.7m-2.64 1.33H3.7' fill='none' stroke='%23000' stroke-width='.4'/%3E%3Cpath d='M4.37 288.8v7.94m0-4.11h3.96' fill='none' stroke='%23000' stroke-width='.3'/%3E%3Ctext font-weight='700' font-size='3.17' font-family='sans-serif'%3E%3Ctspan x='5.11' y='291.96'%3EF%3C/tspan%3E%3Ctspan x='5.03' y='295.68'%3EB%3C/tspan%3E%3C/text%3E%3C/g%3E%3C/svg%3E%0A");
}
button#bom-btn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8.47 8.47'%3E%3Crect transform='translate(0 -288.53)' ry='1.17' y='288.8' x='.27' height='7.94' width='7.94' fill='%23f9f9f9'/%3E%3Cg transform='translate(0 -288.53)' fill='none' stroke='%23000' stroke-width='.4'%3E%3Crect width='7.94' height='7.94' x='.27' y='288.8' ry='1.17' stroke-linejoin='round'/%3E%3Cpath d='M1.59 290.12h5.29M1.59 291.45h5.33M1.59 292.75h5.33M1.59 294.09h5.33M1.59 295.41h5.33'/%3E%3C/g%3E%3C/svg%3E");
}
button#bom-grouped-btn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Cg stroke='%23000' stroke-linejoin='round' class='layer'%3E%3Crect width='29' height='29' x='1.5' y='1.5' stroke-width='2' fill='%23fff' rx='5' ry='5'/%3E%3Cpath stroke-linecap='square' stroke-width='2' d='M6 10h4m4 0h5m4 0h3M6.1 22h3m3.9 0h5m4 0h4m-16-8h4m4 0h4'/%3E%3Cpath stroke-linecap='null' d='M5 17.5h22M5 26.6h22M5 5.5h22'/%3E%3C/g%3E%3C/svg%3E");
}
button#bom-ungrouped-btn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Cg stroke='%23000' stroke-linejoin='round' class='layer'%3E%3Crect width='29' height='29' x='1.5' y='1.5' stroke-width='2' fill='%23fff' rx='5' ry='5'/%3E%3Cpath stroke-linecap='square' stroke-width='2' d='M6 10h4m-4 8h3m-3 8h4'/%3E%3Cpath stroke-linecap='null' d='M5 13.5h22m-22 8h22M5 5.5h22'/%3E%3C/g%3E%3C/svg%3E");
}
button#bom-netlist-btn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Cg fill='none' stroke='%23000' class='layer'%3E%3Crect width='29' height='29' x='1.5' y='1.5' stroke-width='2' fill='%23fff' rx='5' ry='5'/%3E%3Cpath stroke-width='2' d='M6 26l6-6v-8m13.8-6.3l-6 6v8'/%3E%3Ccircle cx='11.8' cy='9.5' r='2.8' stroke-width='2'/%3E%3Ccircle cx='19.8' cy='22.8' r='2.8' stroke-width='2'/%3E%3C/g%3E%3C/svg%3E");
}
button#copy {
background-image: url("data:image/svg+xml,%3Csvg height='48' viewBox='0 0 48 48' width='48' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h48v48h-48z' fill='none'/%3E%3Cpath d='M32 2h-24c-2.21 0-4 1.79-4 4v28h4v-28h24v-4zm6 8h-22c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h22c2.21 0 4-1.79 4-4v-28c0-2.21-1.79-4-4-4zm0 32h-22v-28h22v28z'/%3E%3C/svg%3E");
background-position: 6px 6px;
background-repeat: no-repeat;
background-size: 26px 26px;
border-radius: 6px;
height: 40px;
width: 40px;
margin: 10px 5px;
}
button#copy:active {
box-shadow: inset 0px 0px 5px #6c6c6c;
}
textarea.clipboard-temp {
position: fixed;
top: 0;
left: 0;
width: 2em;
height: 2em;
padding: 0;
border: None;
outline: None;
box-shadow: None;
background: transparent;
}
.left-most-button {
border-right: 0;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
}
.middle-button {
border-right: 0;
}
.right-most-button {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
.button-container {
font-size: 0;
margin: 10px 10px 10px 0px;
}
.dark .button-container {
filter: invert(1);
}
.button-container button {
background-size: 32px 32px;
background-position: 5px 5px;
background-repeat: no-repeat;
}
@media print {
.hideonprint {
display: none;
}
}
canvas {
cursor: crosshair;
}
canvas:active {
cursor: grabbing;
}
.fileinfo {
width: 100%;
max-width: 1000px;
border: none;
padding: 5px;
}
.fileinfo .title {
font-size: 20pt;
font-weight: bold;
}
.fileinfo td {
overflow: hidden;
white-space: nowrap;
max-width: 1px;
width: 50%;
text-overflow: ellipsis;
}
.bom {
border-collapse: collapse;
font-family: Consolas, "DejaVu Sans Mono", Monaco, monospace;
font-size: 10pt;
table-layout: fixed;
width: 100%;
margin-top: 1px;
position: relative;
}
.bom th,
.bom td {
border: 1px solid black;
padding: 5px;
word-wrap: break-word;
text-align: center;
position: relative;
}
.dark .bom th,
.dark .bom td {
border: 1px solid #777;
}
.bom th {
background-color: #CCCCCC;
background-clip: padding-box;
}
.dark .bom th {
background-color: #3b4749;
}
.bom tr.highlighted:nth-child(n) {
background-color: #cfc;
}
.dark .bom tr.highlighted:nth-child(n) {
background-color: #226022;
}
.bom tr:nth-child(even) {
background-color: #f2f2f2;
}
.dark .bom tr:nth-child(even) {
background-color: #313b40;
}
.bom tr.checked {
color: #1cb53d;
}
.dark .bom tr.checked {
color: #2cce54;
}
.bom tr {
transition: background-color 0.2s;
}
.bom .numCol {
width: 5%;
}
.bom .references {
width: 10%;
}
.bom .quantity {
width: 10%;
}
.bom th .sortmark {
position: absolute;
right: 1px;
top: 1px;
margin-top: -5px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent #221 transparent;
transform-origin: 50% 85%;
transition: opacity 0.2s, transform 0.4s;
}
.dark .bom th .sortmark {
filter: invert(1);
}
.bom th .sortmark.none {
opacity: 0;
}
.bom th .sortmark.desc {
transform: rotate(180deg);
}
.bom th:hover .sortmark.none {
opacity: 0.5;
}
.bom .bom-checkbox {
width: 30px;
position: relative;
user-select: none;
-moz-user-select: none;
}
.bom .bom-checkbox:before {
content: "";
position: absolute;
border-width: 15px;
border-style: solid;
border-color: #51829f transparent transparent transparent;
visibility: hidden;
top: -15px;
}
.bom .bom-checkbox:after {
content: "Double click to set/unset all";
position: absolute;
color: white;
top: -35px;
left: -26px;
background: #51829f;
padding: 5px 15px;
border-radius: 8px;
white-space: nowrap;
visibility: hidden;
}
.bom .bom-checkbox:hover:before,
.bom .bom-checkbox:hover:after {
visibility: visible;
transition: visibility 0.2s linear 1s;
}
.split {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow-y: auto;
overflow-x: hidden;
background-color: inherit;
}
.split.split-horizontal,
.gutter.gutter-horizontal {
height: 100%;
float: left;
}
.gutter {
background-color: #ddd;
background-repeat: no-repeat;
background-position: 50%;
transition: background-color 0.3s;
}
.dark .gutter {
background-color: #777;
}
.gutter.gutter-horizontal {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
cursor: ew-resize;
width: 5px;
}
.gutter.gutter-vertical {
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
cursor: ns-resize;
height: 5px;
}
.searchbox {
float: left;
height: 40px;
margin: 10px 5px;
padding: 12px 32px;
font-family: Consolas, "DejaVu Sans Mono", Monaco, monospace;
font-size: 18px;
box-sizing: border-box;
border: 1px solid #888;
border-radius: 6px;
outline: none;
background-color: #eee;
transition: background-color 0.2s, border 0.2s;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABNklEQVQ4T8XSMUvDQBQH8P/LElFa/AIZHcTBQSz0I/gFstTBRR2KUC4ldDxw7h0Bl3RRUATxi4iiODgoiLNrbQYp5J6cpJJqomkX33Z37/14d/dIa33MzDuYI4johOI4XhyNRteO46zNYjDzAxE1yBZprVeZ+QbAUhXEGJMA2Ox2u4+fQIa0mPmsCgCgJYQ4t7lfgF0opQYAdv9ABkKI/UnOFCClXKjX61cA1osQY8x9kiRNKeV7IWA3oyhaSdP0FkAtjxhj3hzH2RBCPOf3pzqYHCilfAAX+URm9oMguPzeWSGQvUcMYC8rOBJCHBRdqxTo9/vbRHRqi8bj8XKv1xvODbiuW2u32/bvf0SlDv4XYOY7z/Mavu+nM1+BmQ+NMc0wDF/LprP0DbTWW0T00ul0nn4b7Q87+X4Qmfiq2wAAAABJRU5ErkJggg==');
background-position: 10px 10px;
background-repeat: no-repeat;
}
.dark .searchbox {
background-color: #111;
color: #eee;
}
.searchbox::placeholder {
color: #ccc;
}
.dark .searchbox::placeholder {
color: #666;
}
.filter {
width: calc(60% - 64px);
}
.reflookup {
width: calc(40% - 10px);
}
input[type=text]:focus {
background-color: white;
border: 1px solid #333;
}
.dark input[type=text]:focus {
background-color: #333;
border: 1px solid #ccc;
}
mark.highlight {
background-color: #5050ff;
color: #fff;
padding: 2px;
border-radius: 6px;
}
.dark mark.highlight {
background-color: #76a6da;
color: #111;
}
.menubtn {
background-color: white;
border: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36' viewBox='0 0 20 20'%3E%3Cpath fill='none' d='M0 0h20v20H0V0z'/%3E%3Cpath d='M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z'/%3E%3C/svg%3E%0A");
background-position: center;
background-repeat: no-repeat;
}
.statsbtn {
background-color: white;
border: none;
background-image: url("data:image/svg+xml,%3Csvg width='36' height='36' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 6h28v24H4V6zm0 8h28v8H4m9-16v24h10V5.8' fill='none' stroke='%23000' stroke-width='2'/%3E%3C/svg%3E");
background-position: center;
background-repeat: no-repeat;
}
.iobtn {
background-color: white;
border: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36'%3E%3Cpath fill='none' stroke='%23000' stroke-width='2' d='M3 33v-7l6.8-7h16.5l6.7 7v7H3zM3.2 26H33M21 9l5-5.9 5 6h-2.5V15h-5V9H21zm-4.9 0l-5 6-5-6h2.5V3h5v6h2.5z'/%3E%3Cpath fill='none' stroke='%23000' d='M6.1 29.5H10'/%3E%3C/svg%3E");
background-position: center;
background-repeat: no-repeat;
}
.visbtn {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Cpath fill='none' stroke='%23333' d='M2.5 4.5h5v15h-5zM9.5 4.5h5v15h-5zM16.5 4.5h5v15h-5z'/%3E%3C/svg%3E");
background-position: center;
background-repeat: no-repeat;
padding: 15px;
}
#vismenu-content {
left: 0px;
font-family: Verdana, sans-serif;
}
.dark .statsbtn,
.dark .savebtn,
.dark .menubtn,
.dark .iobtn,
.dark .visbtn {
filter: invert(1);
}
.flexbox {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.savebtn {
background-color: #d6d6d6;
width: auto;
height: 30px;
flex-grow: 1;
margin: 5px;
border-radius: 4px;
}
.savebtn:active {
background-color: #0a0;
color: white;
}
.dark .savebtn:active {
/* This will be inverted */
background-color: #b3b;
}
.stats {
border-collapse: collapse;
font-size: 12pt;
table-layout: fixed;
width: 100%;
min-width: 450px;
}
.dark .stats td {
border: 1px solid #bbb;
}
.stats td {
border: 1px solid black;
padding: 5px;
word-wrap: break-word;
text-align: center;
position: relative;
}
#checkbox-stats div {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
#checkbox-stats .bar {
background-color: rgba(28, 251, 0, 0.6);
}
.menu {
position: relative;
display: inline-block;
margin: 10px 10px 10px 0px;
}
.menu-content {
font-size: 12pt !important;
text-align: left !important;
font-weight: normal !important;
display: none;
position: absolute;
background-color: white;
right: 0;
min-width: 300px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 100;
padding: 8px;
}
.dark .menu-content {
background-color: #111;
}
.menu:hover .menu-content {
display: block;
}
.menu:hover .menubtn,
.menu:hover .iobtn,
.menu:hover .statsbtn {
background-color: #eee;
}
.menu-label {
display: inline-block;
padding: 8px;
border: 1px solid #ccc;
border-top: 0;
width: calc(100% - 18px);
}
.menu-label-top {
border-top: 1px solid #ccc;
}
.menu-textbox {
float: left;
height: 24px;
margin: 10px 5px;
padding: 5px 5px;
font-family: Consolas, "DejaVu Sans Mono", Monaco, monospace;
font-size: 14px;
box-sizing: border-box;
border: 1px solid #888;
border-radius: 4px;
outline: none;
background-color: #eee;
transition: background-color 0.2s, border 0.2s;
width: calc(100% - 10px);
}
.menu-textbox.invalid,
.dark .menu-textbox.invalid {
color: red;
}
.dark .menu-textbox {
background-color: #222;
color: #eee;
}
.radio-container {
margin: 4px;
}
.topmostdiv {
width: 100%;
height: 100%;
background-color: white;
transition: background-color 0.3s;
}
#top {
height: 78px;
border-bottom: 2px solid black;
}
.dark #top {
border-bottom: 2px solid #ccc;
}
#dbg {
display: block;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #aaa;
}
::-webkit-scrollbar-thumb {
background: #666;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
.slider {
-webkit-appearance: none;
width: 100%;
margin: 3px 0;
padding: 0;
outline: none;
opacity: 0.7;
-webkit-transition: .2s;
transition: opacity .2s;
border-radius: 3px;
}
.slider:hover {
opacity: 1;
}
.slider:focus {
outline: none;
}
.slider::-webkit-slider-runnable-track {
-webkit-appearance: none;
width: 100%;
height: 8px;
background: #d3d3d3;
border-radius: 3px;
border: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 15px;
height: 15px;
border-radius: 50%;
background: #0a0;
cursor: pointer;
margin-top: -4px;
}
.dark .slider::-webkit-slider-thumb {
background: #3d3;
}
.slider::-moz-range-thumb {
width: 15px;
height: 15px;
border-radius: 50%;
background: #0a0;
cursor: pointer;
}
.slider::-moz-range-track {
height: 8px;
background: #d3d3d3;
border-radius: 3px;
}
.dark .slider::-moz-range-thumb {
background: #3d3;
}
.slider::-ms-track {
width: 100%;
height: 8px;
border-width: 3px 0;
background: transparent;
border-color: transparent;
color: transparent;
transition: opacity .2s;
}
.slider::-ms-fill-lower {
background: #d3d3d3;
border: none;
border-radius: 3px;
}
.slider::-ms-fill-upper {
background: #d3d3d3;
border: none;
border-radius: 3px;
}
.slider::-ms-thumb {
width: 15px;
height: 15px;
border-radius: 50%;
background: #0a0;
cursor: pointer;
margin: 0;
}
.shameless-plug {
font-size: 0.8em;
text-align: center;
display: block;
}
a {
color: #0278a4;
}
.dark a {
color: #00b9fd;
}
#frontcanvas,
#backcanvas {
touch-action: none;
}
.placeholder {
border: 1px dashed #9f9fda !important;
background-color: #edf2f7 !important;
}
.dragging {
z-index: 999;
}
.dark .dragging>table>tbody>tr {
background-color: #252c30;
}
.dark .placeholder {
filter: invert(1);
}
.column-spacer {
top: 0;
left: 0;
width: calc(100% - 4px);
position: absolute;
cursor: pointer;
user-select: none;
height: 100%;
}
.column-width-handle {
top: 0;
right: 0;
width: 4px;
position: absolute;
cursor: col-resize;
user-select: none;
height: 100%;
}
.column-width-handle:hover {
background-color: #4f99bd;
}
:root {
}
.dark.topmostdiv {
}
</style>
<script type="text/javascript" >
///////////////////////////////////////////////
/*
Split.js - v1.3.5
MIT License
https://github.com/nathancahill/Split.js
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Split=t()}(this,function(){"use strict";var e=window,t=e.document,n="addEventListener",i="removeEventListener",r="getBoundingClientRect",s=function(){return!1},o=e.attachEvent&&!e[n],a=["","-webkit-","-moz-","-o-"].filter(function(e){var n=t.createElement("div");return n.style.cssText="width:"+e+"calc(9px)",!!n.style.length}).shift()+"calc",l=function(e){return"string"==typeof e||e instanceof String?t.querySelector(e):e};return function(u,c){function z(e,t,n){var i=A(y,t,n);Object.keys(i).forEach(function(t){return e.style[t]=i[t]})}function h(e,t){var n=B(y,t);Object.keys(n).forEach(function(t){return e.style[t]=n[t]})}function f(e){var t=E[this.a],n=E[this.b],i=t.size+n.size;t.size=e/this.size*i,n.size=i-e/this.size*i,z(t.element,t.size,this.aGutterSize),z(n.element,n.size,this.bGutterSize)}function m(e){var t;this.dragging&&((t="touches"in e?e.touches[0][b]-this.start:e[b]-this.start)<=E[this.a].minSize+M+this.aGutterSize?t=E[this.a].minSize+this.aGutterSize:t>=this.size-(E[this.b].minSize+M+this.bGutterSize)&&(t=this.size-(E[this.b].minSize+this.bGutterSize)),f.call(this,t),c.onDrag&&c.onDrag())}function g(){var e=E[this.a].element,t=E[this.b].element;this.size=e[r]()[y]+t[r]()[y]+this.aGutterSize+this.bGutterSize,this.start=e[r]()[G]}function d(){var t=this,n=E[t.a].element,r=E[t.b].element;t.dragging&&c.onDrag&&c.onDrag(),t.dragging=!1,e[i]("mouseup",t.stop),e[i]("touchend",t.stop),e[i]("touchcancel",t.stop),t.parent[i]("mousemove",t.move),t.parent[i]("touchmove",t.move),delete t.stop,delete t.move,n[i]("selectstart",s),n[i]("dragstart",s),r[i]("selectstart",s),r[i]("dragstart",s),n.style.userSelect="",n.style.webkitUserSelect="",n.style.MozUserSelect="",n.style.pointerEvents="",r.style.userSelect="",r.style.webkitUserSelect="",r.style.MozUserSelect="",r.style.pointerEvents="",t.gutter.style.cursor="",t.parent.style.cursor=""}function S(t){var i=this,r=E[i.a].element,o=E[i.b].element;!i.dragging&&c.onDragStart&&c.onDragStart(),t.preventDefault(),i.dragging=!0,i.move=m.bind(i),i.stop=d.bind(i),e[n]("mouseup",i.stop),e[n]("touchend",i.stop),e[n]("touchcancel",i.stop),i.parent[n]("mousemove",i.move),i.parent[n]("touchmove",i.move),r[n]("selectstart",s),r[n]("dragstart",s),o[n]("selectstart",s),o[n]("dragstart",s),r.style.userSelect="none",r.style.webkitUserSelect="none",r.style.MozUserSelect="none",r.style.pointerEvents="none",o.style.userSelect="none",o.style.webkitUserSelect="none",o.style.MozUserSelect="none",o.style.pointerEvents="none",i.gutter.style.cursor=j,i.parent.style.cursor=j,g.call(i)}function v(e){e.forEach(function(t,n){if(n>0){var i=F[n-1],r=E[i.a],s=E[i.b];r.size=e[n-1],s.size=t,z(r.element,r.size,i.aGutterSize),z(s.element,s.size,i.bGutterSize)}})}function p(){F.forEach(function(e){e.parent.removeChild(e.gutter),E[e.a].element.style[y]="",E[e.b].element.style[y]=""})}void 0===c&&(c={});var y,b,G,E,w=l(u[0]).parentNode,D=e.getComputedStyle(w).flexDirection,U=c.sizes||u.map(function(){return 100/u.length}),k=void 0!==c.minSize?c.minSize:100,x=Array.isArray(k)?k:u.map(function(){return k}),L=void 0!==c.gutterSize?c.gutterSize:10,M=void 0!==c.snapOffset?c.snapOffset:30,O=c.direction||"horizontal",j=c.cursor||("horizontal"===O?"ew-resize":"ns-resize"),C=c.gutter||function(e,n){var i=t.createElement("div");return i.className="gutter gutter-"+n,i},A=c.elementStyle||function(e,t,n){var i={};return"string"==typeof t||t instanceof String?i[e]=t:i[e]=o?t+"%":a+"("+t+"% - "+n+"px)",i},B=c.gutterStyle||function(e,t){return n={},n[e]=t+"px",n;var n};"horizontal"===O?(y="width","clientWidth",b="clientX",G="left","paddingLeft"):"vertical"===O&&(y="height","clientHeight",b="clientY",G="top","paddingTop");var F=[];return E=u.map(function(e,t){var i,s={element:l(e),size:U[t],minSize:x[t]};if(t>0&&(i={a:t-1,b:t,dragging:!1,isFirst:1===t,isLast:t===u.length-1,direction:O,parent:w},i.aGutterSize=L,i.bGutterSize=L,i.isFirst&&(i.aGutterSize=L/2),i.isLast&&(i.bGutterSize=L/2),"row-reverse"===D||"column-reverse"===D)){var a=i.a;i.a=i.b,i.b=a}if(!o&&t>0){var c=C(t,O);h(c,L),c[n]("mousedown",S.bind(i)),c[n]("touchstart",S.bind(i)),w.insertBefore(c,s.element),i.gutter=c}0===t||t===u.length-1?z(s.element,s.size,L/2):z(s.element,s.size,L);var f=s.element[r]()[y];return f<s.minSize&&(s.minSize=f),t>0&&F.push(i),s}),o?{setSizes:v,destroy:p}:{setSizes:v,getSizes:function(){return E.map(function(e){return e.size})},collapse:function(e){if(e===F.length){var t=F[e-1];g.call(t),o||f.call(t,t.size-t.bGutterSize)}else{var n=F[e];g.call(n),o||f.call(n,n.aGutterSize)}},destroy:p}}});
///////////////////////////////////////////////
///////////////////////////////////////////////
// Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
// This work is free. You can redistribute it and/or modify it
// under the terms of the WTFPL, Version 2
// For more information see LICENSE.txt or http://www.wtfpl.net/
//
// For more information, the home page:
// http://pieroxy.net/blog/pages/lz-string/testing.html
//
// LZ-based compression algorithm, version 1.4.4
var LZString=function(){var o=String.fromCharCode,i={};var n={decompressFromBase64:function(o){return null==o?"":""==o?null:n._decompress(o.length,32,function(n){return function(o,n){if(!i[o]){i[o]={};for(var t=0;t<o.length;t++)i[o][o.charAt(t)]=t}return i[o][n]}("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",o.charAt(n))})},_decompress:function(i,n,t){var r,e,a,s,p,u,l,f=[],c=4,d=4,h=3,v="",g=[],m={val:t(0),position:n,index:1};for(r=0;r<3;r+=1)f[r]=r;for(a=0,p=Math.pow(2,2),u=1;u!=p;)s=m.val&m.position,m.position>>=1,0==m.position&&(m.position=n,m.val=t(m.index++)),a|=(s>0?1:0)*u,u<<=1;switch(a){case 0:for(a=0,p=Math.pow(2,8),u=1;u!=p;)s=m.val&m.position,m.position>>=1,0==m.position&&(m.position=n,m.val=t(m.index++)),a|=(s>0?1:0)*u,u<<=1;l=o(a);break;case 1:for(a=0,p=Math.pow(2,16),u=1;u!=p;)s=m.val&m.position,m.position>>=1,0==m.position&&(m.position=n,m.val=t(m.index++)),a|=(s>0?1:0)*u,u<<=1;l=o(a);break;case 2:return""}for(f[3]=l,e=l,g.push(l);;){if(m.index>i)return"";for(a=0,p=Math.pow(2,h),u=1;u!=p;)s=m.val&m.position,m.position>>=1,0==m.position&&(m.position=n,m.val=t(m.index++)),a|=(s>0?1:0)*u,u<<=1;switch(l=a){case 0:for(a=0,p=Math.pow(2,8),u=1;u!=p;)s=m.val&m.position,m.position>>=1,0==m.position&&(m.position=n,m.val=t(m.index++)),a|=(s>0?1:0)*u,u<<=1;f[d++]=o(a),l=d-1,c--;break;case 1:for(a=0,p=Math.pow(2,16),u=1;u!=p;)s=m.val&m.position,m.position>>=1,0==m.position&&(m.position=n,m.val=t(m.index++)),a|=(s>0?1:0)*u,u<<=1;f[d++]=o(a),l=d-1,c--;break;case 2:return g.join("")}if(0==c&&(c=Math.pow(2,h),h++),f[l])v=f[l];else{if(l!==d)return null;v=e+e.charAt(0)}g.push(v),f[d++]=e+v.charAt(0),e=v,0==--c&&(c=Math.pow(2,h),h++)}}};return n}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString});
///////////////////////////////////////////////
///////////////////////////////////////////////
/*!
* PEP v0.4.3 | https://github.com/jquery/PEP
* Copyright jQuery Foundation and other contributors | http://jquery.org/license
*/
!function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.PointerEventsPolyfill=b()}(this,function(){"use strict";function a(a,b){b=b||Object.create(null);var c=document.createEvent("Event");c.initEvent(a,b.bubbles||!1,b.cancelable||!1);
for(var d,e=2;e<m.length;e++)d=m[e],c[d]=b[d]||n[e];c.buttons=b.buttons||0;
var f=0;return f=b.pressure&&c.buttons?b.pressure:c.buttons?.5:0,c.x=c.clientX,c.y=c.clientY,c.pointerId=b.pointerId||0,c.width=b.width||0,c.height=b.height||0,c.pressure=f,c.tiltX=b.tiltX||0,c.tiltY=b.tiltY||0,c.twist=b.twist||0,c.tangentialPressure=b.tangentialPressure||0,c.pointerType=b.pointerType||"",c.hwTimestamp=b.hwTimestamp||0,c.isPrimary=b.isPrimary||!1,c}function b(){this.array=[],this.size=0}function c(a,b,c,d){this.addCallback=a.bind(d),this.removeCallback=b.bind(d),this.changedCallback=c.bind(d),A&&(this.observer=new A(this.mutationWatcher.bind(this)))}function d(a){return"body /shadow-deep/ "+e(a)}function e(a){return'[touch-action="'+a+'"]'}function f(a){return"{ -ms-touch-action: "+a+"; touch-action: "+a+"; }"}function g(){if(F){D.forEach(function(a){String(a)===a?(E+=e(a)+f(a)+"\n",G&&(E+=d(a)+f(a)+"\n")):(E+=a.selectors.map(e)+f(a.rule)+"\n",G&&(E+=a.selectors.map(d)+f(a.rule)+"\n"))});var a=document.createElement("style");a.textContent=E,document.head.appendChild(a)}}function h(){if(!window.PointerEvent){if(window.PointerEvent=a,window.navigator.msPointerEnabled){var b=window.navigator.msMaxTouchPoints;Object.defineProperty(window.navigator,"maxTouchPoints",{value:b,enumerable:!0}),u.registerSource("ms",_)}else Object.defineProperty(window.navigator,"maxTouchPoints",{value:0,enumerable:!0}),u.registerSource("mouse",N),void 0!==window.ontouchstart&&u.registerSource("touch",V);u.register(document)}}function i(a){if(!u.pointermap.has(a)){var b=new Error("InvalidPointerId");throw b.name="InvalidPointerId",b}}function j(a){for(var b=a.parentNode;b&&b!==a.ownerDocument;)b=b.parentNode;if(!b){var c=new Error("InvalidStateError");throw c.name="InvalidStateError",c}}function k(a){var b=u.pointermap.get(a);return 0!==b.buttons}function l(){window.Element&&!Element.prototype.setPointerCapture&&Object.defineProperties(Element.prototype,{setPointerCapture:{value:W},releasePointerCapture:{value:X},hasPointerCapture:{value:Y}})}
var m=["bubbles","cancelable","view","detail","screenX","screenY","clientX","clientY","ctrlKey","altKey","shiftKey","metaKey","button","relatedTarget","pageX","pageY"],n=[!1,!1,null,null,0,0,0,0,!1,!1,!1,!1,0,null,0,0],o=window.Map&&window.Map.prototype.forEach,p=o?Map:b;b.prototype={set:function(a,b){return void 0===b?this["delete"](a):(this.has(a)||this.size++,void(this.array[a]=b))},has:function(a){return void 0!==this.array[a]},"delete":function(a){this.has(a)&&(delete this.array[a],this.size--)},get:function(a){return this.array[a]},clear:function(){this.array.length=0,this.size=0},forEach:function(a,b){return this.array.forEach(function(c,d){a.call(b,c,d,this)},this)}};var q=["bubbles","cancelable","view","detail","screenX","screenY","clientX","clientY","ctrlKey","altKey","shiftKey","metaKey","button","relatedTarget","buttons","pointerId","width","height","pressure","tiltX","tiltY","pointerType","hwTimestamp","isPrimary","type","target","currentTarget","which","pageX","pageY","timeStamp"],r=[!1,!1,null,null,0,0,0,0,!1,!1,!1,!1,0,null,0,0,0,0,0,0,0,"",0,!1,"",null,null,0,0,0,0],s={pointerover:1,pointerout:1,pointerenter:1,pointerleave:1},t="undefined"!=typeof SVGElementInstance,u={pointermap:new p,eventMap:Object.create(null),captureInfo:Object.create(null),eventSources:Object.create(null),eventSourceList:[],registerSource:function(a,b){var c=b,d=c.events;d&&(d.forEach(function(a){c[a]&&(this.eventMap[a]=c[a].bind(c))},this),this.eventSources[a]=c,this.eventSourceList.push(c))},register:function(a){for(var b,c=this.eventSourceList.length,d=0;d<c&&(b=this.eventSourceList[d]);d++)
b.register.call(b,a)},unregister:function(a){for(var b,c=this.eventSourceList.length,d=0;d<c&&(b=this.eventSourceList[d]);d++)
b.unregister.call(b,a)},contains:function(a,b){try{return a.contains(b)}catch(c){return!1}},down:function(a){a.bubbles=!0,this.fireEvent("pointerdown",a)},move:function(a){a.bubbles=!0,this.fireEvent("pointermove",a)},up:function(a){a.bubbles=!0,this.fireEvent("pointerup",a)},enter:function(a){a.bubbles=!1,this.fireEvent("pointerenter",a)},leave:function(a){a.bubbles=!1,this.fireEvent("pointerleave",a)},over:function(a){a.bubbles=!0,this.fireEvent("pointerover",a)},out:function(a){a.bubbles=!0,this.fireEvent("pointerout",a)},cancel:function(a){a.bubbles=!0,this.fireEvent("pointercancel",a)},leaveOut:function(a){this.out(a),this.propagate(a,this.leave,!1)},enterOver:function(a){this.over(a),this.propagate(a,this.enter,!0)},eventHandler:function(a){if(!a._handledByPE){var b=a.type,c=this.eventMap&&this.eventMap[b];c&&c(a),a._handledByPE=!0}},listen:function(a,b){b.forEach(function(b){this.addEvent(a,b)},this)},unlisten:function(a,b){b.forEach(function(b){this.removeEvent(a,b)},this)},addEvent:function(a,b){a.addEventListener(b,this.boundHandler)},removeEvent:function(a,b){a.removeEventListener(b,this.boundHandler)},makeEvent:function(b,c){this.captureInfo[c.pointerId]&&(c.relatedTarget=null);var d=new a(b,c);return c.preventDefault&&(d.preventDefault=c.preventDefault),d._target=d._target||c.target,d},fireEvent:function(a,b){var c=this.makeEvent(a,b);return this.dispatchEvent(c)},cloneEvent:function(a){for(var b,c=Object.create(null),d=0;d<q.length;d++)b=q[d],c[b]=a[b]||r[d],!t||"target"!==b&&"relatedTarget"!==b||c[b]instanceof SVGElementInstance&&(c[b]=c[b].correspondingUseElement);return a.preventDefault&&(c.preventDefault=function(){a.preventDefault()}),c},getTarget:function(a){var b=this.captureInfo[a.pointerId];return b?a._target!==b&&a.type in s?void 0:b:a._target},propagate:function(a,b,c){for(var d=a.target,e=[];d!==document&&!d.contains(a.relatedTarget);) if(e.push(d),d=d.parentNode,!d)return;c&&e.reverse(),e.forEach(function(c){a.target=c,b.call(this,a)},this)},setCapture:function(b,c,d){this.captureInfo[b]&&this.releaseCapture(b,d),this.captureInfo[b]=c,this.implicitRelease=this.releaseCapture.bind(this,b,d),document.addEventListener("pointerup",this.implicitRelease),document.addEventListener("pointercancel",this.implicitRelease);var e=new a("gotpointercapture");e.pointerId=b,e._target=c,d||this.asyncDispatchEvent(e)},releaseCapture:function(b,c){var d=this.captureInfo[b];if(d){this.captureInfo[b]=void 0,document.removeEventListener("pointerup",this.implicitRelease),document.removeEventListener("pointercancel",this.implicitRelease);var e=new a("lostpointercapture");e.pointerId=b,e._target=d,c||this.asyncDispatchEvent(e)}},dispatchEvent:/*scope.external.dispatchEvent || */function(a){var b=this.getTarget(a);if(b)return b.dispatchEvent(a)},asyncDispatchEvent:function(a){requestAnimationFrame(this.dispatchEvent.bind(this,a))}};u.boundHandler=u.eventHandler.bind(u);var v={shadow:function(a){if(a)return a.shadowRoot||a.webkitShadowRoot},canTarget:function(a){return a&&Boolean(a.elementFromPoint)},targetingShadow:function(a){var b=this.shadow(a);if(this.canTarget(b))return b},olderShadow:function(a){var b=a.olderShadowRoot;if(!b){var c=a.querySelector("shadow");c&&(b=c.olderShadowRoot)}return b},allShadows:function(a){for(var b=[],c=this.shadow(a);c;)b.push(c),c=this.olderShadow(c);return b},searchRoot:function(a,b,c){if(a){var d,e,f=a.elementFromPoint(b,c);for(e=this.targetingShadow(f);e;){if(d=e.elementFromPoint(b,c)){var g=this.targetingShadow(d);return this.searchRoot(g,b,c)||d} e=this.olderShadow(e)} return f}},owner:function(a){
for(var b=a;b.parentNode;)b=b.parentNode;
return b.nodeType!==Node.DOCUMENT_NODE&&b.nodeType!==Node.DOCUMENT_FRAGMENT_NODE&&(b=document),b},findTarget:function(a){var b=a.clientX,c=a.clientY,d=this.owner(a.target);
return d.elementFromPoint(b,c)||(d=document),this.searchRoot(d,b,c)}},w=Array.prototype.forEach.call.bind(Array.prototype.forEach),x=Array.prototype.map.call.bind(Array.prototype.map),y=Array.prototype.slice.call.bind(Array.prototype.slice),z=Array.prototype.filter.call.bind(Array.prototype.filter),A=window.MutationObserver||window.WebKitMutationObserver,B="[touch-action]",C={subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0,attributeFilter:["touch-action"]};c.prototype={watchSubtree:function(a){
//
this.observer&&v.canTarget(a)&&this.observer.observe(a,C)},enableOnSubtree:function(a){this.watchSubtree(a),a===document&&"complete"!==document.readyState?this.installOnLoad():this.installNewSubtree(a)},installNewSubtree:function(a){w(this.findElements(a),this.addElement,this)},findElements:function(a){return a.querySelectorAll?a.querySelectorAll(B):[]},removeElement:function(a){this.removeCallback(a)},addElement:function(a){this.addCallback(a)},elementChanged:function(a,b){this.changedCallback(a,b)},concatLists:function(a,b){return a.concat(y(b))},
installOnLoad:function(){document.addEventListener("readystatechange",function(){"complete"===document.readyState&&this.installNewSubtree(document)}.bind(this))},isElement:function(a){return a.nodeType===Node.ELEMENT_NODE},flattenMutationTree:function(a){
var b=x(a,this.findElements,this);
return b.push(z(a,this.isElement)),b.reduce(this.concatLists,[])},mutationWatcher:function(a){a.forEach(this.mutationHandler,this)},mutationHandler:function(a){if("childList"===a.type){var b=this.flattenMutationTree(a.addedNodes);b.forEach(this.addElement,this);var c=this.flattenMutationTree(a.removedNodes);c.forEach(this.removeElement,this)}else"attributes"===a.type&&this.elementChanged(a.target,a.oldValue)}};var D=["none","auto","pan-x","pan-y",{rule:"pan-x pan-y",selectors:["pan-x pan-y","pan-y pan-x"]}],E="",F=window.PointerEvent||window.MSPointerEvent,G=!window.ShadowDOMPolyfill&&document.head.createShadowRoot,H=u.pointermap,I=25,J=[1,4,2,8,16],K=!1;try{K=1===new MouseEvent("test",{buttons:1}).buttons}catch(L){}
var M,N={POINTER_ID:1,POINTER_TYPE:"mouse",events:["mousedown","mousemove","mouseup","mouseover","mouseout"],register:function(a){u.listen(a,this.events)},unregister:function(a){u.unlisten(a,this.events)},lastTouches:[],
isEventSimulatedFromTouch:function(a){for(var b,c=this.lastTouches,d=a.clientX,e=a.clientY,f=0,g=c.length;f<g&&(b=c[f]);f++){
var h=Math.abs(d-b.x),i=Math.abs(e-b.y);if(h<=I&&i<=I)return!0}},prepareEvent:function(a){var b=u.cloneEvent(a),c=b.preventDefault;return b.preventDefault=function(){a.preventDefault(),c()},b.pointerId=this.POINTER_ID,b.isPrimary=!0,b.pointerType=this.POINTER_TYPE,b},prepareButtonsForMove:function(a,b){var c=H.get(this.POINTER_ID);
0!==b.which&&c?a.buttons=c.buttons:a.buttons=0,b.buttons=a.buttons},mousedown:function(a){if(!this.isEventSimulatedFromTouch(a)){var b=H.get(this.POINTER_ID),c=this.prepareEvent(a);K||(c.buttons=J[c.button],b&&(c.buttons|=b.buttons),a.buttons=c.buttons),H.set(this.POINTER_ID,a),b&&0!==b.buttons?u.move(c):u.down(c)}},mousemove:function(a){if(!this.isEventSimulatedFromTouch(a)){var b=this.prepareEvent(a);K||this.prepareButtonsForMove(b,a),b.button=-1,H.set(this.POINTER_ID,a),u.move(b)}},mouseup:function(a){if(!this.isEventSimulatedFromTouch(a)){var b=H.get(this.POINTER_ID),c=this.prepareEvent(a);if(!K){var d=J[c.button];
c.buttons=b?b.buttons&~d:0,a.buttons=c.buttons}H.set(this.POINTER_ID,a),
c.buttons&=~J[c.button],0===c.buttons?u.up(c):u.move(c)}},mouseover:function(a){if(!this.isEventSimulatedFromTouch(a)){var b=this.prepareEvent(a);K||this.prepareButtonsForMove(b,a),b.button=-1,H.set(this.POINTER_ID,a),u.enterOver(b)}},mouseout:function(a){if(!this.isEventSimulatedFromTouch(a)){var b=this.prepareEvent(a);K||this.prepareButtonsForMove(b,a),b.button=-1,u.leaveOut(b)}},cancel:function(a){var b=this.prepareEvent(a);u.cancel(b),this.deactivateMouse()},deactivateMouse:function(){H["delete"](this.POINTER_ID)}},O=u.captureInfo,P=v.findTarget.bind(v),Q=v.allShadows.bind(v),R=u.pointermap,S=2500,T=200,U="touch-action",V={events:["touchstart","touchmove","touchend","touchcancel"],register:function(a){M.enableOnSubtree(a)},unregister:function(){},elementAdded:function(a){var b=a.getAttribute(U),c=this.touchActionToScrollType(b);c&&(a._scrollType=c,u.listen(a,this.events),
Q(a).forEach(function(a){a._scrollType=c,u.listen(a,this.events)},this))},elementRemoved:function(a){a._scrollType=void 0,u.unlisten(a,this.events),
Q(a).forEach(function(a){a._scrollType=void 0,u.unlisten(a,this.events)},this)},elementChanged:function(a,b){var c=a.getAttribute(U),d=this.touchActionToScrollType(c),e=this.touchActionToScrollType(b);
d&&e?(a._scrollType=d,Q(a).forEach(function(a){a._scrollType=d},this)):e?this.elementRemoved(a):d&&this.elementAdded(a)},scrollTypes:{EMITTER:"none",XSCROLLER:"pan-x",YSCROLLER:"pan-y",SCROLLER:/^(?:pan-x pan-y)|(?:pan-y pan-x)|auto$/},touchActionToScrollType:function(a){var b=a,c=this.scrollTypes;return"none"===b?"none":b===c.XSCROLLER?"X":b===c.YSCROLLER?"Y":c.SCROLLER.exec(b)?"XY":void 0},POINTER_TYPE:"touch",firstTouch:null,isPrimaryTouch:function(a){return this.firstTouch===a.identifier},setPrimaryTouch:function(a){
(0===R.size||1===R.size&&R.has(1))&&(this.firstTouch=a.identifier,this.firstXY={X:a.clientX,Y:a.clientY},this.scrolling=!1,this.cancelResetClickCount())},removePrimaryPointer:function(a){a.isPrimary&&(this.firstTouch=null,this.firstXY=null,this.resetClickCount())},clickCount:0,resetId:null,resetClickCount:function(){var a=function(){this.clickCount=0,this.resetId=null}.bind(this);this.resetId=setTimeout(a,T)},cancelResetClickCount:function(){this.resetId&&clearTimeout(this.resetId)},typeToButtons:function(a){var b=0;return"touchstart"!==a&&"touchmove"!==a||(b=1),b},touchToPointer:function(a){var b=this.currentTouchEvent,c=u.cloneEvent(a),d=c.pointerId=a.identifier+2;c.target=O[d]||P(c),c.bubbles=!0,c.cancelable=!0,c.detail=this.clickCount,c.button=0,c.buttons=this.typeToButtons(b.type),c.width=2*(a.radiusX||a.webkitRadiusX||0),c.height=2*(a.radiusY||a.webkitRadiusY||0),c.pressure=a.force||a.webkitForce||.5,c.isPrimary=this.isPrimaryTouch(a),c.pointerType=this.POINTER_TYPE,
c.altKey=b.altKey,c.ctrlKey=b.ctrlKey,c.metaKey=b.metaKey,c.shiftKey=b.shiftKey;
var e=this;return c.preventDefault=function(){e.scrolling=!1,e.firstXY=null,b.preventDefault()},c},processTouches:function(a,b){var c=a.changedTouches;this.currentTouchEvent=a;for(var d,e=0;e<c.length;e++)d=c[e],b.call(this,this.touchToPointer(d))},
shouldScroll:function(a){if(this.firstXY){var b,c=a.currentTarget._scrollType;if("none"===c)
b=!1;else if("XY"===c)
b=!0;else{var d=a.changedTouches[0],e=c,f="Y"===c?"X":"Y",g=Math.abs(d["client"+e]-this.firstXY[e]),h=Math.abs(d["client"+f]-this.firstXY[f]);
b=g>=h}return this.firstXY=null,b}},findTouch:function(a,b){for(var c,d=0,e=a.length;d<e&&(c=a[d]);d++)if(c.identifier===b)return!0},
vacuumTouches:function(a){var b=a.touches;
if(R.size>=b.length){var c=[];R.forEach(function(a,d){
if(1!==d&&!this.findTouch(b,d-2)){var e=a.out;c.push(e)}},this),c.forEach(this.cancelOut,this)}},touchstart:function(a){this.vacuumTouches(a),this.setPrimaryTouch(a.changedTouches[0]),this.dedupSynthMouse(a),this.scrolling||(this.clickCount++,this.processTouches(a,this.overDown))},overDown:function(a){R.set(a.pointerId,{target:a.target,out:a,outTarget:a.target}),u.enterOver(a),u.down(a)},touchmove:function(a){this.scrolling||(this.shouldScroll(a)?(this.scrolling=!0,this.touchcancel(a)):(a.preventDefault(),this.processTouches(a,this.moveOverOut)))},moveOverOut:function(a){var b=a,c=R.get(b.pointerId);
if(c){var d=c.out,e=c.outTarget;u.move(b),d&&e!==b.target&&(d.relatedTarget=b.target,b.relatedTarget=e,
d.target=e,b.target?(u.leaveOut(d),u.enterOver(b)):(
b.target=e,b.relatedTarget=null,this.cancelOut(b))),c.out=b,c.outTarget=b.target}},touchend:function(a){this.dedupSynthMouse(a),this.processTouches(a,this.upOut)},upOut:function(a){this.scrolling||(u.up(a),u.leaveOut(a)),this.cleanUpPointer(a)},touchcancel:function(a){this.processTouches(a,this.cancelOut)},cancelOut:function(a){u.cancel(a),u.leaveOut(a),this.cleanUpPointer(a)},cleanUpPointer:function(a){R["delete"](a.pointerId),this.removePrimaryPointer(a)},
dedupSynthMouse:function(a){var b=N.lastTouches,c=a.changedTouches[0];
if(this.isPrimaryTouch(c)){
var d={x:c.clientX,y:c.clientY};b.push(d);var e=function(a,b){var c=a.indexOf(b);c>-1&&a.splice(c,1)}.bind(null,b,d);setTimeout(e,S)}}};M=new c(V.elementAdded,V.elementRemoved,V.elementChanged,V);var W,X,Y,Z=u.pointermap,$=window.MSPointerEvent&&"number"==typeof window.MSPointerEvent.MSPOINTER_TYPE_MOUSE,_={events:["MSPointerDown","MSPointerMove","MSPointerUp","MSPointerOut","MSPointerOver","MSPointerCancel","MSGotPointerCapture","MSLostPointerCapture"],register:function(a){u.listen(a,this.events)},unregister:function(a){u.unlisten(a,this.events)},POINTER_TYPES:["","unavailable","touch","pen","mouse"],prepareEvent:function(a){var b=a;return $&&(b=u.cloneEvent(a),b.pointerType=this.POINTER_TYPES[a.pointerType]),b},cleanup:function(a){Z["delete"](a)},MSPointerDown:function(a){Z.set(a.pointerId,a);var b=this.prepareEvent(a);u.down(b)},MSPointerMove:function(a){var b=this.prepareEvent(a);u.move(b)},MSPointerUp:function(a){var b=this.prepareEvent(a);u.up(b),this.cleanup(a.pointerId)},MSPointerOut:function(a){var b=this.prepareEvent(a);u.leaveOut(b)},MSPointerOver:function(a){var b=this.prepareEvent(a);u.enterOver(b)},MSPointerCancel:function(a){var b=this.prepareEvent(a);u.cancel(b),this.cleanup(a.pointerId)},MSLostPointerCapture:function(a){var b=u.makeEvent("lostpointercapture",a);u.dispatchEvent(b)},MSGotPointerCapture:function(a){var b=u.makeEvent("gotpointercapture",a);u.dispatchEvent(b)}},aa=window.navigator;aa.msPointerEnabled?(W=function(a){i(a),j(this),k(a)&&(u.setCapture(a,this,!0),this.msSetPointerCapture(a))},X=function(a){i(a),u.releaseCapture(a,!0),this.msReleasePointerCapture(a)}):(W=function(a){i(a),j(this),k(a)&&u.setCapture(a,this)},X=function(a){i(a),u.releaseCapture(a)}),Y=function(a){return!!u.captureInfo[a]},g(),h(),l();var ba={dispatcher:u,Installer:c,PointerEvent:a,PointerMap:p,targetFinding:v};return ba});
///////////////////////////////////////////////
///////////////////////////////////////////////
var config = {"bommode":"grouped","show_fabrication":false,"redraw_on_drag":true,"highlight_pin1":false,"fields":["References","Value","[Comment]","[Description]","[Footprint]","Qty"],"hiddenColumns":[],"extra_fields":["[Comment]","[Description]","[Footprint]"],"dark_mode":false,"bom_view":"left-right","board_rotation":0,"checkboxes":"Placed","show_silkscreen":true,"show_pads":true,"layer_view":"FB","groupdes":false};
///////////////////////////////////////////////
///////////////////////////////////////////////
var pcbdata = JSON.parse(LZString.decompressFromBase64('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kogNTf6bqvEpx1e6lCqBqo4X6qbA8eSlgKqhSyqrYqmyoarziEREarnK9HB7t2hbqsFQuq4SqeAXsvqq2qhuQavWrxQtqr2qjqtav4EFzGatoq5qxioWrCEJauaqSvTatCq600apOreqxxhOrWq3auerxq46p+qvqhSnOrzKiREaqaqzkDqrbq0KHurhADivSRVqwGtCqwsJ6oc9kPO6m6rkyA6q2qMa16sRrTqwGqmqIVS6usrN8CGv6J7Kyyscrlq9JABqyKLGurSzq3GveqhqlGoBrma5shpr0a36rOqCa1lSJqwaiYBuqyaxaopqUqmGrlx4a2mtCqnxZGuRdMaqWrxq2atFJlqla16p5qxVPmuuq7K4WpirRagXjlwOaraolhlao2rRrDarmo+r5ag2strNqtWreENakmvmqhau6pFqmqsWuBIJa7qsCFlar2tNrQqvETlqqSPGslrA6j2qBqoqkGpdr+amoEFq2ccMB1rXavWuBIrawOverg64EiZrPa82sVqE2b2vNraa22pFF7a+isdrY6kCHjqqahGhxqOGVGpfFuq4MgDqAqeGsVrK62uq2qW6iarDrKqiOvjqo6+EBjqEiOOqsqK6vOrrr6ahGurqM6tuqzrR65OoCoDagur9Ei6+KpLqB6suqHqHq1VSDruqqPGVqd632qchpTfetcBD61utCqT6juoXroxJetqqV6lPBury6jeuo0Sq/CpUrAq6CsKrQqp+o/qXK5+r8rgq7KsvrL4a+vBrb66bGhrE66StyroUZRiZttafKtQr1lIyrdRf6yBqbT8q5BsKrAGwFGAaBarWudqH6t2p6p1sytOrrPYw1J0qAqMhvFIKGohtdgu6auuIaaG+00yzvgLBsYgcG6Orwaoal2qpqHnSDJobAhFhq2rBG09IEals6rOEbxGqS0kbGGzuumru66qs1rEqjfHAbYa9yCEb5aqhq7LNGuhpob1G0RrNrZG+Wtka2G0yA4a+6rhuYqeGjes7cNGlGq0aSG5si0b6G5snWyXG9JCMaUakxrPg7ahRtmqHa/urvrB6ymo3remOxvRwHGmhsFRdGk6rcaomqRu0anIMJr8zWGnxsLq/Gq6oCbLG+aoIbE6koioyWfYXx5TAq5+SuStxLaoEUymkpoGLgsCppqadPUxvdBzG0mtLrVGpJvqa9NdptuTCm9HG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'));
///////////////////////////////////////////////
///////////////////////////////////////////////
/* Utility functions */
var storagePrefix = 'KiCad_HTML_BOM__' + pcbdata.metadata.title + '__' +
pcbdata.metadata.revision + '__#';
var storage;
function initStorage(key) {
try {
window.localStorage.getItem("blank");
storage = window.localStorage;
} catch (e) {
// localStorage not available
}
if (!storage) {
try {
window.sessionStorage.getItem("blank");
storage = window.sessionStorage;
} catch (e) {
// sessionStorage also not available
}
}
}
function readStorage(key) {
if (storage) {
return storage.getItem(storagePrefix + key);
} else {
return null;
}
}
function writeStorage(key, value) {
if (storage) {
storage.setItem(storagePrefix + key, value);
}
}
function fancyDblClickHandler(el, onsingle, ondouble) {
return function() {
if (el.getAttribute("data-dblclick") == null) {
el.setAttribute("data-dblclick", 1);
setTimeout(function() {
if (el.getAttribute("data-dblclick") == 1) {
onsingle();
}
el.removeAttribute("data-dblclick");
}, 200);
} else {
el.removeAttribute("data-dblclick");
ondouble();
}
}
}
function smoothScrollToRow(rowid) {
document.getElementById(rowid).scrollIntoView({
behavior: "smooth",
block: "center",
inline: "nearest"
});
}
function focusInputField(input) {
input.scrollIntoView(false);
input.focus();
input.select();
}
function copyToClipboard() {
var text = '';
for (var node of bomhead.childNodes[0].childNodes) {
if (node.firstChild) {
text = text + node.firstChild.nodeValue;
}
if (node != bomhead.childNodes[0].lastChild) {
text += '\t';
}
}
text += '\n';
for (var row of bombody.childNodes) {
for (var cell of row.childNodes) {
for (var node of cell.childNodes) {
if (node.nodeName == "INPUT") {
if (node.checked) {
text = text + '✓';
}
} else if (node.nodeName == "MARK") {
text = text + node.firstChild.nodeValue;
} else {
text = text + node.nodeValue;
}
}
if (cell != row.lastChild) {
text += '\t';
}
}
text += '\n';
}
var textArea = document.createElement("textarea");
textArea.classList.add('clipboard-temp');
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
if (document.execCommand('copy')) {
console.log('Bom copied to clipboard.');
}
} catch (err) {
console.log('Can not copy to clipboard.');
}
document.body.removeChild(textArea);
}
function removeGutterNode(node) {
for (var i = 0; i < node.childNodes.length; i++) {
if (node.childNodes[i].classList &&
node.childNodes[i].classList.contains("gutter")) {
node.removeChild(node.childNodes[i]);
break;
}
}
}
function cleanGutters() {
removeGutterNode(document.getElementById("bot"));
removeGutterNode(document.getElementById("canvasdiv"));
}
var units = {
prefixes: {
giga: ["G", "g", "giga", "Giga", "GIGA", "Г"],
mega: ["M", "mega", "Mega", "MEGA", "М"],
kilo: ["K", "k", "kilo", "Kilo", "KILO", "к"],
milli: ["m", "milli", "Milli", "MILLI", "мл"],
micro: ["U", "u", "micro", "Micro", "MICRO", "μ", "µ", "мк"], // different utf8 μ
nano: ["N", "n", "nano", "Nano", "NANO", "н"],
pico: ["P", "p", "pico", "Pico", "PICO", "п"],
},
unitsShort: ["R", "r", "Ω", "F", "f", "H", "h"],
unitsLong: [
"OHM", "Ohm", "ohm", "ohms",
"FARAD", "Farad", "farad",
"HENRY", "Henry", "henry"
],
getMultiplier: function(s) {
if (this.prefixes.giga.includes(s)) return 1e9;
if (this.prefixes.mega.includes(s)) return 1e6;
if (this.prefixes.kilo.includes(s)) return 1e3;
if (this.prefixes.milli.includes(s)) return 1e-3;
if (this.prefixes.micro.includes(s)) return 1e-6;
if (this.prefixes.nano.includes(s)) return 1e-9;
if (this.prefixes.pico.includes(s)) return 1e-12;
return 1;
},
valueRegex: null,
}
function initUtils() {
var allPrefixes = units.prefixes.giga
.concat(units.prefixes.mega)
.concat(units.prefixes.kilo)
.concat(units.prefixes.milli)
.concat(units.prefixes.micro)
.concat(units.prefixes.nano)
.concat(units.prefixes.pico);
var allUnits = units.unitsShort.concat(units.unitsLong);
units.valueRegex = new RegExp("^([0-9\.]+)" +
"\\s*(" + allPrefixes.join("|") + ")?" +
"(" + allUnits.join("|") + ")?" +
"(\\b.*)?$", "");
units.valueAltRegex = new RegExp("^([0-9]*)" +
"(" + units.unitsShort.join("|") + ")?" +
"([GgMmKkUuNnPp])?" +
"([0-9]*)" +
"(\\b.*)?$", "");
for (var bom_type of ["both", "F", "B"]) {
for (var row of pcbdata.bom[bom_type]) {
row.push(parseValue(row[1], row[3][0][0]));
}
}
}
function parseValue(val, ref) {
var inferUnit = (unit, ref) => {
if (unit) {
unit = unit.toLowerCase();
if (unit == 'Ω' || unit == "ohm" || unit == "ohms") {
unit = 'r';
}
unit = unit[0];
} else {
ref = /^([a-z]+)\d+$/i.exec(ref);
if (ref) {
ref = ref[1].toLowerCase();
if (ref == "c") unit = 'f';
else if (ref == "l") unit = 'h';
else if (ref == "r" || ref == "rv") unit = 'r';
else unit = null;
}
}
return unit;
};
val = val.replace(/,/g, "");
var match = units.valueRegex.exec(val);
var unit;
if (match) {
val = parseFloat(match[1]);
if (match[2]) {
val = val * units.getMultiplier(match[2]);
}
unit = inferUnit(match[3], ref);
if (!unit) return null;
else return {
val: val,
unit: unit,
extra: match[4],
}
}
match = units.valueAltRegex.exec(val);
if (match && (match[1] || match[4])) {
val = parseFloat(match[1] + "." + match[4]);
if (match[3]) {
val = val * units.getMultiplier(match[3]);
}
unit = inferUnit(match[2], ref);
if (!unit) return null;
else return {
val: val,
unit: unit,
extra: match[5],
}
}
return null;
}
function valueCompare(a, b, stra, strb) {
if (a === null && b === null) {
// Failed to parse both values, compare them as strings.
if (stra != strb) return stra > strb ? 1 : -1;
else return 0;
} else if (a === null) {
return 1;
} else if (b === null) {
return -1;
} else {
if (a.unit != b.unit) return a.unit > b.unit ? 1 : -1;
else if (a.val != b.val) return a.val > b.val ? 1 : -1;
else if (a.extra != b.extra) return a.extra > b.extra ? 1 : -1;
else return 0;
}
}
function refCompare(a, b) {
var regex = new RegExp('^([A-Za-z]*)(\\d+)');
var desa = a[0];
var desb = b[0];
if (desa.match(regex) && desb.match(regex))
{
var bsplitted = regex.exec(desb);
var asplitted = regex.exec(desa);
var c = asplitted[1].localeCompare(bsplitted[1]);
if (c!==0) return c;
var aint = parseInt(asplitted[2]);
var bint = parseInt(bsplitted[2]);
if (aint !== bint) return aint > bint ? 1 : -1;
else return 0;
}
else
{
return desa.localeCompare(desb);
}
}
function validateSaveImgDimension(element) {
var valid = false;
var intValue = 0;
if (/^[1-9]\d*$/.test(element.value)) {
intValue = parseInt(element.value);
if (intValue <= 16000) {
valid = true;
}
}
if (valid) {
element.classList.remove("invalid");
} else {
element.classList.add("invalid");
}
return intValue;
}
function saveImage(layer) {
var width = validateSaveImgDimension(document.getElementById("render-save-width"));
var height = validateSaveImgDimension(document.getElementById("render-save-height"));
var bgcolor = null;
if (!document.getElementById("render-save-transparent").checked) {
var style = getComputedStyle(topmostdiv);
bgcolor = style.getPropertyValue("background-color");
}
if (!width || !height) return;
// Prepare image
var canvas = document.createElement("canvas");
var layerdict = {
transform: {
x: 0,
y: 0,
s: 1,
panx: 0,
pany: 0,
zoom: 1,
},
bg: canvas,
fab: canvas,
silk: canvas,
highlight: canvas,
layer: layer,
}
// Do the rendering
recalcLayerScale(layerdict, width, height);
prepareLayer(layerdict);
clearCanvas(canvas, bgcolor);
drawBackground(layerdict, false);
drawHighlightsOnLayer(layerdict, false);
// Save image
var imgdata = canvas.toDataURL("image/png");
var filename = pcbdata.metadata.title;
if (pcbdata.metadata.revision) {
filename += `.${pcbdata.metadata.revision}`;
}
filename += `.${layer}.png`;
saveFile(filename, dataURLtoBlob(imgdata));
}
function saveSettings() {
var data = {
type: "InteractiveHtmlBom settings",
version: 1,
pcbmetadata: pcbdata.metadata,
settings: settings,
}
var blob = new Blob([JSON.stringify(data, null, 4)], {
type: "application/json"
});
saveFile(`${pcbdata.metadata.title}.settings.json`, blob);
}
function loadSettings() {
var input = document.createElement("input");
input.type = "file";
input.accept = ".settings.json";
input.onchange = function(e) {
var file = e.target.files[0];
var reader = new FileReader();
reader.onload = readerEvent => {
var content = readerEvent.target.result;
var newSettings;
try {
newSettings = JSON.parse(content);
} catch (e) {
alert("Selected file is not InteractiveHtmlBom settings file.");
return;
}
if (newSettings.type != "InteractiveHtmlBom settings") {
alert("Selected file is not InteractiveHtmlBom settings file.");
return;
}
var metadataMatches = newSettings.hasOwnProperty("pcbmetadata");
if (metadataMatches) {
for (var k in pcbdata.metadata) {
if (!newSettings.pcbmetadata.hasOwnProperty(k) || newSettings.pcbmetadata[k] != pcbdata.metadata[k]) {
metadataMatches = false;
}
}
}
if (!metadataMatches) {
var currentMetadata = JSON.stringify(pcbdata.metadata, null, 4);
var fileMetadata = JSON.stringify(newSettings.pcbmetadata, null, 4);
if (!confirm(
`Settins file metadata does not match current metadata.\n\n` +
`Page metadata:\n${currentMetadata}\n\n` +
`Settings file metadata:\n${fileMetadata}\n\n` +
`Press OK if you would like to import settings anyway.`)) {
return;
}
}
overwriteSettings(newSettings.settings);
}
reader.readAsText(file, 'UTF-8');
}
input.click();
}
function overwriteSettings(newSettings) {
initDone = false;
Object.assign(settings, newSettings);
writeStorage("bomlayout", settings.bomlayout);
writeStorage("bommode", settings.bommode);
writeStorage("canvaslayout", settings.canvaslayout);
writeStorage("bomCheckboxes", settings.checkboxes.join(","));
document.getElementById("bomCheckboxes").value = settings.checkboxes.join(",");
for (var checkbox of settings.checkboxes) {
writeStorage("checkbox_" + checkbox, settings.checkboxStoredRefs[checkbox]);
}
writeStorage("markWhenChecked", settings.markWhenChecked);
padsVisible(settings.renderPads);
document.getElementById("padsCheckbox").checked = settings.renderPads;
fabricationVisible(settings.renderFabrication);
document.getElementById("fabricationCheckbox").checked = settings.renderFabrication;
silkscreenVisible(settings.renderSilkscreen);
document.getElementById("silkscreenCheckbox").checked = settings.renderSilkscreen;
referencesVisible(settings.renderReferences);
document.getElementById("referencesCheckbox").checked = settings.renderReferences;
valuesVisible(settings.renderValues);
document.getElementById("valuesCheckbox").checked = settings.renderValues;
tracksVisible(settings.renderTracks);
document.getElementById("tracksCheckbox").checked = settings.renderTracks;
zonesVisible(settings.renderZones);
document.getElementById("zonesCheckbox").checked = settings.renderZones;
dnpOutline(settings.renderDnpOutline);
document.getElementById("dnpOutlineCheckbox").checked = settings.renderDnpOutline;
setRedrawOnDrag(settings.redrawOnDrag);
document.getElementById("dragCheckbox").checked = settings.redrawOnDrag;
setDarkMode(settings.darkMode);
document.getElementById("darkmodeCheckbox").checked = settings.darkMode;
setHighlightPin1(settings.highlightpin1);
document.getElementById("highlightpin1Checkbox").checked = settings.highlightpin1;
showFootprints(settings.show_footprints);
writeStorage("boardRotation", settings.boardRotation);
document.getElementById("boardRotation").value = settings.boardRotation / 5;
document.getElementById("rotationDegree").textContent = settings.boardRotation;
initDone = true;
prepCheckboxes();
changeBomLayout(settings.bomlayout);
}
function saveFile(filename, blob) {
var link = document.createElement("a");
var objurl = URL.createObjectURL(blob);
link.download = filename;
link.href = objurl;
link.click();
}
function dataURLtoBlob(dataurl) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], {
type: mime
});
}
var settings = {
canvaslayout: "default",
bomlayout: "default",
bommode: "grouped",
checkboxes: [],
checkboxStoredRefs: {},
darkMode: false,
highlightpin1: false,
redrawOnDrag: true,
boardRotation: 0,
renderPads: true,
renderReferences: true,
renderValues: true,
renderSilkscreen: true,
renderFabrication: true,
renderDnpOutline: false,
renderTracks: true,
renderZones: true,
}
function initDefaults() {
settings.bomlayout = readStorage("bomlayout");
if (settings.bomlayout === null) {
settings.bomlayout = config.bom_view;
}
if (!['bom-only', 'left-right', 'top-bottom'].includes(settings.bomlayout)) {
settings.bomlayout = config.bom_view;
}
settings.bommode = readStorage("bommode");
if (settings.bommode === null) {
settings.bommode = "grouped";
}
if (!["grouped", "ungrouped", "netlist"].includes(settings.bommode)) {
settings.bommode = "grouped";
}
settings.canvaslayout = readStorage("canvaslayout");
if (settings.canvaslayout === null) {
settings.canvaslayout = config.layer_view;
}
var bomCheckboxes = readStorage("bomCheckboxes");
if (bomCheckboxes === null) {
bomCheckboxes = config.checkboxes;
}
settings.checkboxes = bomCheckboxes.split(",").filter((e) => e);
document.getElementById("bomCheckboxes").value = bomCheckboxes;
settings.markWhenChecked = readStorage("markWhenChecked") || "";
populateMarkWhenCheckedOptions();
function initBooleanSetting(storageString, def, elementId, func) {
var b = readStorage(storageString);
if (b === null) {
b = def;
} else {
b = (b == "true");
}
document.getElementById(elementId).checked = b;
func(b);
}
initBooleanSetting("padsVisible", config.show_pads, "padsCheckbox", padsVisible);
initBooleanSetting("fabricationVisible", config.show_fabrication, "fabricationCheckbox", fabricationVisible);
initBooleanSetting("silkscreenVisible", config.show_silkscreen, "silkscreenCheckbox", silkscreenVisible);
initBooleanSetting("referencesVisible", true, "referencesCheckbox", referencesVisible);
initBooleanSetting("valuesVisible", true, "valuesCheckbox", valuesVisible);
if ("tracks" in pcbdata) {
initBooleanSetting("tracksVisible", true, "tracksCheckbox", tracksVisible);
initBooleanSetting("zonesVisible", true, "zonesCheckbox", zonesVisible);
} else {
document.getElementById("tracksAndZonesCheckboxes").style.display = "none";
tracksVisible(false);
zonesVisible(false);
}
initBooleanSetting("dnpOutline", false, "dnpOutlineCheckbox", dnpOutline);
initBooleanSetting("redrawOnDrag", config.redraw_on_drag, "dragCheckbox", setRedrawOnDrag);
initBooleanSetting("darkmode", config.dark_mode, "darkmodeCheckbox", setDarkMode);
initBooleanSetting("highlightpin1", config.highlight_pin1, "highlightpin1Checkbox", setHighlightPin1);
var fields = ["checkboxes"].concat(config.fields);
var hcols = JSON.parse(readStorage("hiddenColumns"));
if (hcols === null) {
hcols = [];
}
settings.hiddenColumns = config.hiddenColumns;
//settings.hiddenColumns = hcols.filter(e => fields.includes(e));
var cord = JSON.parse(readStorage("columnOrder"));
if (cord === null) {
cord = fields;
} else {
cord = cord.filter(e => fields.includes(e));
if (cord.length != fields.length)
cord = fields;
}
settings.columnOrder = cord;
settings.boardRotation = readStorage("boardRotation");
if (settings.boardRotation === null) {
settings.boardRotation = config.board_rotation * 5;
} else {
settings.boardRotation = parseInt(settings.boardRotation);
}
document.getElementById("boardRotation").value = settings.boardRotation / 5;
document.getElementById("rotationDegree").textContent = settings.boardRotation;
}
// Helper classes for user js callbacks.
const IBOM_EVENT_TYPES = {
ALL: "all",
HIGHLIGHT_EVENT: "highlightEvent",
CHECKBOX_CHANGE_EVENT: "checkboxChangeEvent",
BOM_BODY_CHANGE_EVENT: "bomBodyChangeEvent",
}
const EventHandler = {
callbacks: {},
init: function() {
for (eventType of Object.values(IBOM_EVENT_TYPES))
this.callbacks[eventType] = [];
},
registerCallback: function(eventType, callback) {
this.callbacks[eventType].push(callback);
},
emitEvent: function(eventType, eventArgs) {
event = {
eventType: eventType,
args: eventArgs,
}
var callback;
for (callback of this.callbacks[eventType])
callback(event);
for (callback of this.callbacks[IBOM_EVENT_TYPES.ALL])
callback(event);
}
}
EventHandler.init();
///////////////////////////////////////////////
///////////////////////////////////////////////
/* PCB rendering code */
var emptyContext2d = document.createElement("canvas").getContext("2d");
function deg2rad(deg) {
return deg * Math.PI / 180;
}
function calcFontPoint(linepoint, text, offsetx, offsety, tilt) {
var point = [
linepoint[0] * text.width + offsetx,
linepoint[1] * text.height + offsety
];
// This approximates pcbnew behavior with how text tilts depending on horizontal justification
point[0] -= (linepoint[1] + 0.5 * (1 + text.justify[0])) * text.height * tilt;
return point;
}
function drawText(ctx, text, color) {
if ("ref" in text && !settings.renderReferences) return;
if ("val" in text && !settings.renderValues) return;
ctx.save();
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.lineWidth = text.thickness;
if ("svgpath" in text) {
ctx.stroke(new Path2D(text.svgpath));
ctx.restore();
return;
}
ctx.translate(...text.pos);
ctx.translate(text.thickness * 0.5, 0);
var angle = -text.angle;
if (text.attr.includes("mirrored")) {
ctx.scale(-1, 1);
angle = -angle;
}
var tilt = 0;
if (text.attr.includes("italic")) {
tilt = 0.125;
}
var interline = text.height * 1.5 + text.thickness;
var txt = text.text.split("\n");
// KiCad ignores last empty line.
if (txt[txt.length - 1] == '') txt.pop();
ctx.rotate(deg2rad(angle));
var offsety = (1 - text.justify[1]) / 2 * text.height; // One line offset
offsety -= (txt.length - 1) * (text.justify[1] + 1) / 2 * interline; // Multiline offset
for (var i in txt) {
var lineWidth = text.thickness + interline / 2 * tilt;
for (var j = 0; j < txt[i].length; j++) {
if (txt[i][j] == '\t') {
var fourSpaces = 4 * pcbdata.font_data[' '].w * text.width;
lineWidth += fourSpaces - lineWidth % fourSpaces;
} else {
if (txt[i][j] == '~') {
j++;
if (j == txt[i].length)
break;
}
lineWidth += pcbdata.font_data[txt[i][j]].w * text.width;
}
}
var offsetx = -lineWidth * (text.justify[0] + 1) / 2;
var inOverbar = false;
for (var j = 0; j < txt[i].length; j++) {
if (txt[i][j] == '\t') {
var fourSpaces = 4 * pcbdata.font_data[' '].w * text.width;
offsetx += fourSpaces - offsetx % fourSpaces;
continue;
} else if (txt[i][j] == '~') {
j++;
if (j == txt[i].length)
break;
if (txt[i][j] != '~') {
inOverbar = !inOverbar;
}
}
var glyph = pcbdata.font_data[txt[i][j]];
if (inOverbar) {
var overbarStart = [offsetx, -text.height * 1.4 + offsety];
var overbarEnd = [offsetx + text.width * glyph.w, overbarStart[1]];
if (!lastHadOverbar) {
overbarStart[0] += text.height * 1.4 * tilt;
lastHadOverbar = true;
}
ctx.beginPath();
ctx.moveTo(...overbarStart);
ctx.lineTo(...overbarEnd);
ctx.stroke();
} else {
lastHadOverbar = false;
}
for (var line of glyph.l) {
ctx.beginPath();
ctx.moveTo(...calcFontPoint(line[0], text, offsetx, offsety, tilt));
for (var k = 1; k < line.length; k++) {
ctx.lineTo(...calcFontPoint(line[k], text, offsetx, offsety, tilt));
}
ctx.stroke();
}
offsetx += glyph.w * text.width;
}
offsety += interline;
}
ctx.restore();
}
function drawedge(ctx, scalefactor, edge, color) {
if ("ref" in edge && !settings.renderReferences) return;
if ("val" in edge && !settings.renderValues) return;
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineWidth = Math.max(1 / scalefactor, edge.width);
ctx.lineCap = "round";
ctx.lineJoin = "round";
if ("svgpath" in edge) {
ctx.stroke(new Path2D(edge.svgpath));
} else {
ctx.beginPath();
if (edge.type == "segment") {
ctx.moveTo(...edge.start);
ctx.lineTo(...edge.end);
}
if (edge.type == "rect") {
ctx.moveTo(...edge.start);
ctx.lineTo(edge.start[0], edge.end[1]);
ctx.lineTo(...edge.end);
ctx.lineTo(edge.end[0], edge.start[1]);
ctx.lineTo(...edge.start);
}
if (edge.type == "arc") {
ctx.arc(
...edge.start,
edge.radius,
deg2rad(edge.startangle),
deg2rad(edge.endangle));
}
if (edge.type == "circle") {
ctx.arc(
...edge.start,
edge.radius,
0, 2 * Math.PI);
ctx.closePath();
}
if (edge.type == "curve") {
ctx.moveTo(...edge.start);
ctx.bezierCurveTo(...edge.cpa, ...edge.cpb, ...edge.end);
}
if("filled" in edge && edge.filled)
ctx.fill();
else
ctx.stroke();
}
}
function getChamferedRectPath(size, radius, chamfpos, chamfratio) {
// chamfpos is a bitmask, left = 1, right = 2, bottom left = 4, bottom right = 8
var path = new Path2D();
var width = size[0];
var height = size[1];
var x = width * -0.5;
var y = height * -0.5;
var chamfOffset = Math.min(width, height) * chamfratio;
path.moveTo(x, 0);
if (chamfpos & 4) {
path.lineTo(x, y + height - chamfOffset);
path.lineTo(x + chamfOffset, y + height);
path.lineTo(0, y + height);
} else {
path.arcTo(x, y + height, x + width, y + height, radius);
}
if (chamfpos & 8) {
path.lineTo(x + width - chamfOffset, y + height);
path.lineTo(x + width, y + height - chamfOffset);
path.lineTo(x + width, 0);
} else {
path.arcTo(x + width, y + height, x + width, y, radius);
}
if (chamfpos & 2) {
path.lineTo(x + width, y + chamfOffset);
path.lineTo(x + width - chamfOffset, y);
path.lineTo(0, y);
} else {
path.arcTo(x + width, y, x, y, radius);
}
if (chamfpos & 1) {
path.lineTo(x + chamfOffset, y);
path.lineTo(x, y + chamfOffset);
path.lineTo(x, 0);
} else {
path.arcTo(x, y, x, y + height, radius);
}
path.closePath();
return path;
}
function getOblongPath(size) {
return getChamferedRectPath(size, Math.min(size[0], size[1]) / 2, 0, 0);
}
function getPolygonsPath(shape) {
if (shape.path2d) {
return shape.path2d;
}
if ("svgpath" in shape) {
shape.path2d = new Path2D(shape.svgpath);
} else {
var path = new Path2D();
for (var polygon of shape.polygons) {
path.moveTo(...polygon[0]);
for (var i = 1; i < polygon.length; i++) {
path.lineTo(...polygon[i]);
}
path.closePath();
}
shape.path2d = path;
}
return shape.path2d;
}
function drawPolygonShape(ctx, scalefactor, shape, color) {
if ("ref" in shape && !settings.renderReferences) return;
if ("val" in shape && !settings.renderValues) return;
ctx.save();
if (!("svgpath" in shape)) {
ctx.translate(...shape.pos);
ctx.rotate(deg2rad(-shape.angle));
}
if("filled" in shape && !shape.filled) {
ctx.strokeStyle = color;
ctx.lineWidth = Math.max(1 / scalefactor, shape.width);
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke(getPolygonsPath(shape));
} else {
ctx.fillStyle = color;
ctx.fill(getPolygonsPath(shape));
}
ctx.restore();
}
function drawDrawing(ctx, scalefactor, drawing, color) {
if (["segment", "arc", "circle", "curve", "rect"].includes(drawing.type)) {
drawedge(ctx, scalefactor, drawing, color);
} else if (drawing.type == "polygon") {
drawPolygonShape(ctx, scalefactor, drawing, color);
} else {
drawText(ctx, drawing, color);
}
}
function getCirclePath(radius) {
var path = new Path2D();
path.arc(0, 0, radius, 0, 2 * Math.PI);
path.closePath();
return path;
}
function getCachedPadPath(pad) {
if (!pad.path2d) {
// if path2d is not set, build one and cache it on pad object
if (pad.shape == "rect") {
pad.path2d = new Path2D();
pad.path2d.rect(...pad.size.map(c => -c * 0.5), ...pad.size);
} else if (pad.shape == "oval") {
pad.path2d = getOblongPath(pad.size);
} else if (pad.shape == "circle") {
pad.path2d = getCirclePath(pad.size[0] / 2);
} else if (pad.shape == "roundrect") {
pad.path2d = getChamferedRectPath(pad.size, pad.radius, 0, 0);
} else if (pad.shape == "chamfrect") {
pad.path2d = getChamferedRectPath(pad.size, pad.radius, pad.chamfpos, pad.chamfratio)
} else if (pad.shape == "custom") {
pad.path2d = getPolygonsPath(pad);
}
}
return pad.path2d;
}
function drawPad(ctx, pad, color, outline) {
ctx.save();
ctx.translate(...pad.pos);
ctx.rotate(deg2rad(pad.angle));
if (pad.offset) {
ctx.translate(...pad.offset);
}
ctx.fillStyle = color;
ctx.strokeStyle = color;
var path = getCachedPadPath(pad);
if (outline) {
ctx.stroke(path);
} else {
ctx.fill(path);
}
ctx.restore();
}
function drawPadHole(ctx, pad, padHoleColor) {
if (pad.type != "th") return;
ctx.save();
ctx.translate(...pad.pos);
ctx.rotate(deg2rad(pad.holeangle));
ctx.fillStyle = padHoleColor;
if (pad.drillshape == "oblong") {
ctx.fill(getOblongPath(pad.drillsize));
} else {
ctx.fill(getCirclePath(pad.drillsize[0] / 2));
}
ctx.restore();
}
function drawFootprint(ctx, layer, scalefactor, footprint, colors, highlight, outline) {
if (highlight) {
// draw bounding box
if (footprint.layer == layer) {
ctx.save();
ctx.globalAlpha = 0.2;
ctx.translate(...footprint.bbox.pos);
ctx.rotate(deg2rad(-footprint.bbox.angle));
ctx.translate(...footprint.bbox.relpos);
ctx.fillStyle = colors.pad;
ctx.fillRect(0, 0, ...footprint.bbox.size);
ctx.globalAlpha = 1;
ctx.strokeStyle = colors.pad;
ctx.strokeRect(0, 0, ...footprint.bbox.size);
ctx.restore();
}
}
// draw drawings
for (var drawing of footprint.drawings) {
if (drawing.layer == layer) {
drawDrawing(ctx, scalefactor, drawing.drawing, colors.pad);
}
}
// draw pads
if (settings.renderPads) {
for (var pad of footprint.pads) {
if (pad.layers.includes(layer)) {
drawPad(ctx, pad, colors.pad, outline);
if (pad.pin1 && settings.highlightpin1) {
drawPad(ctx, pad, colors.outline, true);
}
}
}
for (var pad of footprint.pads) {
drawPadHole(ctx, pad, colors.padHole);
}
}
}
function drawEdgeCuts(canvas, scalefactor) {
var ctx = canvas.getContext("2d");
var edgecolor = getComputedStyle(topmostdiv).getPropertyValue('--pcb-edge-color');
for (var edge of pcbdata.edges) {
drawDrawing(ctx, scalefactor, edge, edgecolor);
}
}
function drawFootprints(canvas, layer, scalefactor, highlight) {
var ctx = canvas.getContext("2d");
ctx.lineWidth = 3 / scalefactor;
var style = getComputedStyle(topmostdiv);
var colors = {
pad: style.getPropertyValue('--pad-color'),
padHole: style.getPropertyValue('--pad-hole-color'),
outline: style.getPropertyValue('--pin1-outline-color'),
dnp: style.getPropertyValue('--dnp-color-highlight'),
}
for (var i = 0; i < pcbdata.footprints.length; i++) {
var mod = pcbdata.footprints[i];
var outline = settings.renderDnpOutline && pcbdata.bom.skipped.includes(i);
var h = highlightedFootprints.includes(i);
var d = markedFootprints.has(i);
if (highlight) {
if(h && d) {
colors.pad = style.getPropertyValue('--pad-color-highlight-both');
colors.outline = style.getPropertyValue('--pin1-outline-color-highlight-both');
} else if (h) {
colors.pad = style.getPropertyValue('--pad-color-highlight');
colors.outline = style.getPropertyValue('--pin1-outline-color-highlight');
} else if (d) {
colors.pad = style.getPropertyValue('--pad-color-highlight-marked');
colors.outline = style.getPropertyValue('--pin1-outline-color-highlight-marked');
}
}
if( h || d || !highlight) {
drawFootprint(ctx, layer, scalefactor, mod, colors, highlight, outline);
}
}
}
function drawBgLayer(layername, canvas, layer, scalefactor, edgeColor, polygonColor, textColor) {
var ctx = canvas.getContext("2d");
for (var d of pcbdata.drawings[layername][layer]) {
if (["segment", "arc", "circle", "curve", "rect"].includes(d.type)) {
drawedge(ctx, scalefactor, d, edgeColor);
} else if (d.type == "polygon") {
drawPolygonShape(ctx, scalefactor, d, polygonColor);
} else {
drawText(ctx, d, textColor);
}
}
}
function drawTracks(canvas, layer, color, highlight) {
ctx = canvas.getContext("2d");
ctx.strokeStyle = color;
ctx.lineCap = "round";
for (var track of pcbdata.tracks[layer]) {
if (highlight && highlightedNet != track.net) continue;
ctx.lineWidth = track.width;
ctx.beginPath();
if ('radius' in track) {
ctx.arc(
...track.start,
track.radius,
deg2rad(track.startangle),
deg2rad(track.endangle));
} else {
ctx.moveTo(...track.start);
ctx.lineTo(...track.end);
}
ctx.stroke();
}
}
function drawZones(canvas, layer, color, highlight) {
ctx = canvas.getContext("2d");
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.lineJoin = "round";
for (var zone of pcbdata.zones[layer]) {
if (!zone.path2d) {
zone.path2d = getPolygonsPath(zone);
}
if (highlight && highlightedNet != zone.net) continue;
ctx.fill(zone.path2d);
if (zone.width > 0) {
ctx.lineWidth = zone.width;
ctx.stroke(zone.path2d);
}
}
}
function clearCanvas(canvas, color = null) {
var ctx = canvas.getContext("2d");
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
if (color) {
ctx.fillStyle = color;
ctx.fillRect(0, 0, canvas.width, canvas.height);
} else {
if (!window.matchMedia("print").matches)
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
ctx.restore();
}
function drawNets(canvas, layer, highlight) {
var style = getComputedStyle(topmostdiv);
if (settings.renderTracks) {
var trackColor = style.getPropertyValue(highlight ? '--track-color-highlight' : '--track-color');
drawTracks(canvas, layer, trackColor, highlight);
}
if (settings.renderZones) {
var zoneColor = style.getPropertyValue(highlight ? '--zone-color-highlight' : '--zone-color');
drawZones(canvas, layer, zoneColor, highlight);
}
if (highlight && settings.renderPads) {
var padColor = style.getPropertyValue('--pad-color-highlight');
var padHoleColor = style.getPropertyValue('--pad-hole-color');
var ctx = canvas.getContext("2d");
for (var footprint of pcbdata.footprints) {
// draw pads
var padDrawn = false;
for (var pad of footprint.pads) {
if (highlightedNet != pad.net) continue;
if (pad.layers.includes(layer)) {
drawPad(ctx, pad, padColor, false);
padDrawn = true;
}
}
if (padDrawn) {
// redraw all pad holes because some pads may overlap
for (var pad of footprint.pads) {
drawPadHole(ctx, pad, padHoleColor);
}
}
}
}
}
function drawHighlightsOnLayer(canvasdict, clear = true) {
if (clear) {
clearCanvas(canvasdict.highlight);
}
if (markedFootprints.size > 0 || highlightedFootprints.length > 0) {
drawFootprints(canvasdict.highlight, canvasdict.layer,
canvasdict.transform.s * canvasdict.transform.zoom, true);
}
if (highlightedNet !== null) {
drawNets(canvasdict.highlight, canvasdict.layer, true);
}
}
function drawHighlights() {
drawHighlightsOnLayer(allcanvas.front);
drawHighlightsOnLayer(allcanvas.back);
}
function drawBackground(canvasdict, clear = true) {
if (clear) {
clearCanvas(canvasdict.bg);
clearCanvas(canvasdict.fab);
clearCanvas(canvasdict.silk);
}
drawNets(canvasdict.bg, canvasdict.layer, false);
drawFootprints(canvasdict.bg, canvasdict.layer,
canvasdict.transform.s * canvasdict.transform.zoom, false);
drawEdgeCuts(canvasdict.bg, canvasdict.transform.s * canvasdict.transform.zoom);
var style = getComputedStyle(topmostdiv);
var edgeColor = style.getPropertyValue('--silkscreen-edge-color');
var polygonColor = style.getPropertyValue('--silkscreen-polygon-color');
var textColor = style.getPropertyValue('--silkscreen-text-color');
if (settings.renderSilkscreen) {
drawBgLayer(
"silkscreen", canvasdict.silk, canvasdict.layer,
canvasdict.transform.s * canvasdict.transform.zoom,
edgeColor, polygonColor, textColor);
}
edgeColor = style.getPropertyValue('--fabrication-edge-color');
polygonColor = style.getPropertyValue('--fabrication-polygon-color');
textColor = style.getPropertyValue('--fabrication-text-color');
if (settings.renderFabrication) {
drawBgLayer(
"fabrication", canvasdict.fab, canvasdict.layer,
canvasdict.transform.s * canvasdict.transform.zoom,
edgeColor, polygonColor, textColor);
}
}
function prepareCanvas(canvas, flip, transform) {
var ctx = canvas.getContext("2d");
ctx.setTransform(1, 0, 0, 1, 0, 0);
var fontsize = 1.55;
ctx.scale(transform.zoom, transform.zoom);
ctx.translate(transform.panx, transform.pany);
if (flip) {
ctx.scale(-1, 1);
}
ctx.translate(transform.x, transform.y);
ctx.rotate(deg2rad(settings.boardRotation));
ctx.scale(transform.s, transform.s);
}
function prepareLayer(canvasdict) {
var flip = (canvasdict.layer == "B");
for (var c of ["bg", "fab", "silk", "highlight"]) {
prepareCanvas(canvasdict[c], flip, canvasdict.transform);
}
}
function rotateVector(v, angle) {
angle = deg2rad(angle);
return [
v[0] * Math.cos(angle) - v[1] * Math.sin(angle),
v[0] * Math.sin(angle) + v[1] * Math.cos(angle)
];
}
function applyRotation(bbox) {
var corners = [
[bbox.minx, bbox.miny],
[bbox.minx, bbox.maxy],
[bbox.maxx, bbox.miny],
[bbox.maxx, bbox.maxy],
];
corners = corners.map((v) => rotateVector(v, settings.boardRotation));
return {
minx: corners.reduce((a, v) => Math.min(a, v[0]), Infinity),
miny: corners.reduce((a, v) => Math.min(a, v[1]), Infinity),
maxx: corners.reduce((a, v) => Math.max(a, v[0]), -Infinity),
maxy: corners.reduce((a, v) => Math.max(a, v[1]), -Infinity),
}
}
function recalcLayerScale(layerdict, width, height) {
var bbox = applyRotation(pcbdata.edges_bbox);
var scalefactor = 0.98 * Math.min(
width / (bbox.maxx - bbox.minx),
height / (bbox.maxy - bbox.miny)
);
if (scalefactor < 0.1) {
scalefactor = 1;
}
layerdict.transform.s = scalefactor;
var flip = (layerdict.layer == "B");
if (flip) {
layerdict.transform.x = -((bbox.maxx + bbox.minx) * scalefactor + width) * 0.5;
} else {
layerdict.transform.x = -((bbox.maxx + bbox.minx) * scalefactor - width) * 0.5;
}
layerdict.transform.y = -((bbox.maxy + bbox.miny) * scalefactor - height) * 0.5;
for (var c of ["bg", "fab", "silk", "highlight"]) {
canvas = layerdict[c];
canvas.width = width;
canvas.height = height;
canvas.style.width = (width / devicePixelRatio) + "px";
canvas.style.height = (height / devicePixelRatio) + "px";
}
}
function redrawCanvas(layerdict) {
prepareLayer(layerdict);
drawBackground(layerdict);
drawHighlightsOnLayer(layerdict);
}
function resizeCanvas(layerdict) {
var canvasdivid = {
"F": "frontcanvas",
"B": "backcanvas"
} [layerdict.layer];
var width = document.getElementById(canvasdivid).clientWidth * devicePixelRatio;
var height = document.getElementById(canvasdivid).clientHeight * devicePixelRatio;
recalcLayerScale(layerdict, width, height);
redrawCanvas(layerdict);
}
function resizeAll() {
resizeCanvas(allcanvas.front);
resizeCanvas(allcanvas.back);
}
function pointWithinDistanceToSegment(x, y, x1, y1, x2, y2, d) {
var A = x - x1;
var B = y - y1;
var C = x2 - x1;
var D = y2 - y1;
var dot = A * C + B * D;
var len_sq = C * C + D * D;
var dx, dy;
if (len_sq == 0) {
// start and end of the segment coincide
dx = x - x1;
dy = y - y1;
} else {
var param = dot / len_sq;
var xx, yy;
if (param < 0) {
xx = x1;
yy = y1;
} else if (param > 1) {
xx = x2;
yy = y2;
} else {
xx = x1 + param * C;
yy = y1 + param * D;
}
dx = x - xx;
dy = y - yy;
}
return dx * dx + dy * dy <= d * d;
}
function modulo(n, mod) {
return ((n % mod) + mod) % mod;
}
function pointWithinDistanceToArc(x, y, xc, yc, radius, startangle, endangle, d) {
var dx = x - xc;
var dy = y - yc;
var r_sq = dx * dx + dy * dy;
var rmin = Math.max(0, radius - d);
var rmax = radius + d;
if (r_sq < rmin * rmin || r_sq > rmax * rmax)
return false;
var angle1 = modulo(deg2rad(startangle), 2 * Math.PI);
var dx1 = xc + radius * Math.cos(angle1) - x;
var dy1 = yc + radius * Math.sin(angle1) - y;
if (dx1 * dx1 + dy1 * dy1 <= d * d)
return true;
var angle2 = modulo(deg2rad(endangle), 2 * Math.PI);
var dx2 = xc + radius * Math.cos(angle2) - x;
var dy2 = yc + radius * Math.sin(angle2) - y;
if (dx2 * dx2 + dy2 * dy2 <= d * d)
return true;
var angle = modulo(Math.atan2(dy, dx), 2 * Math.PI);
if (angle1 > angle2)
return (angle >= angle2 || angle <= angle1);
else
return (angle >= angle1 && angle <= angle2);
}
function pointWithinPad(x, y, pad) {
var v = [x - pad.pos[0], y - pad.pos[1]];
v = rotateVector(v, -pad.angle);
if (pad.offset) {
v[0] -= pad.offset[0];
v[1] -= pad.offset[1];
}
return emptyContext2d.isPointInPath(getCachedPadPath(pad), ...v);
}
function netHitScan(layer, x, y) {
// Check track segments
if (settings.renderTracks && pcbdata.tracks) {
for (var track of pcbdata.tracks[layer]) {
if ('radius' in track) {
if (pointWithinDistanceToArc(x, y, ...track.start, track.radius, track.startangle, track.endangle, track.width / 2)) {
return track.net;
}
} else {
if (pointWithinDistanceToSegment(x, y, ...track.start, ...track.end, track.width / 2)) {
return track.net;
}
}
}
}
// Check pads
if (settings.renderPads) {
for (var footprint of pcbdata.footprints) {
for (var pad of footprint.pads) {
if (pad.layers.includes(layer) && pointWithinPad(x, y, pad)) {
return pad.net;
}
}
}
}
return null;
}
function pointWithinFootprintBbox(x, y, bbox) {
var v = [x - bbox.pos[0], y - bbox.pos[1]];
v = rotateVector(v, bbox.angle);
return bbox.relpos[0] <= v[0] && v[0] <= bbox.relpos[0] + bbox.size[0] &&
bbox.relpos[1] <= v[1] && v[1] <= bbox.relpos[1] + bbox.size[1];
}
function bboxHitScan(layer, x, y) {
var result = [];
for (var i = 0; i < pcbdata.footprints.length; i++) {
var footprint = pcbdata.footprints[i];
if (footprint.layer == layer) {
if (pointWithinFootprintBbox(x, y, footprint.bbox)) {
result.push(i);
}
}
}
return result;
}
function handlePointerDown(e, layerdict) {
if (e.button != 0 && e.button != 1) {
return;
}
e.preventDefault();
e.stopPropagation();
if (!e.hasOwnProperty("offsetX")) {
// The polyfill doesn't set this properly
e.offsetX = e.pageX - e.currentTarget.offsetLeft;
e.offsetY = e.pageY - e.currentTarget.offsetTop;
}
layerdict.pointerStates[e.pointerId] = {
distanceTravelled: 0,
lastX: e.offsetX,
lastY: e.offsetY,
downTime: Date.now(),
};
}
function handleMouseClick(e, layerdict) {
if (!e.hasOwnProperty("offsetX")) {
// The polyfill doesn't set this properly
e.offsetX = e.pageX - e.currentTarget.offsetLeft;
e.offsetY = e.pageY - e.currentTarget.offsetTop;
}
var x = e.offsetX;
var y = e.offsetY;
var t = layerdict.transform;
if (layerdict.layer == "B") {
x = (devicePixelRatio * x / t.zoom - t.panx + t.x) / -t.s;
} else {
x = (devicePixelRatio * x / t.zoom - t.panx - t.x) / t.s;
}
y = (devicePixelRatio * y / t.zoom - t.y - t.pany) / t.s;
var v = rotateVector([x, y], -settings.boardRotation);
if (settings.bommode == "netlist") {
if ("nets" in pcbdata) {
var net = netHitScan(layerdict.layer, ...v);
if (net !== highlightedNet) {
netClicked(net);
}
}
}
if (highlightedNet === null) {
var footprints = bboxHitScan(layerdict.layer, ...v);
if (footprints.length > 0) {
footprintsClicked(footprints);
}
else
{
clearHighlightedFootprints();
drawHighlights();
}
}
}
function handlePointerLeave(e, layerdict) {
e.preventDefault();
e.stopPropagation();
if (!settings.redrawOnDrag) {
redrawCanvas(layerdict);
}
delete layerdict.pointerStates[e.pointerId];
}
function resetTransform(layerdict) {
layerdict.transform.panx = 0;
layerdict.transform.pany = 0;
layerdict.transform.zoom = 1;
redrawCanvas(layerdict);
}
function handlePointerUp(e, layerdict) {
if (!e.hasOwnProperty("offsetX")) {
// The polyfill doesn't set this properly
e.offsetX = e.pageX - e.currentTarget.offsetLeft;
e.offsetY = e.pageY - e.currentTarget.offsetTop;
}
e.preventDefault();
e.stopPropagation();
if (e.button == 2) {
// Reset pan and zoom on right click.
resetTransform(layerdict);
layerdict.anotherPointerTapped = false;
return;
}
// We haven't necessarily had a pointermove event since the interaction started, so make sure we update this now
var ptr = layerdict.pointerStates[e.pointerId];
ptr.distanceTravelled += Math.abs(e.offsetX - ptr.lastX) + Math.abs(e.offsetY - ptr.lastY);
if (e.button == 0 && ptr.distanceTravelled < 10 && Date.now() - ptr.downTime <= 500) {
if (Object.keys(layerdict.pointerStates).length == 1) {
if (layerdict.anotherPointerTapped) {
// This is the second pointer coming off of a two-finger tap
resetTransform(layerdict);
} else {
// This is just a regular tap
handleMouseClick(e, layerdict);
}
layerdict.anotherPointerTapped = false;
} else {
// This is the first finger coming off of what could become a two-finger tap
layerdict.anotherPointerTapped = true;
}
} else {
if (!settings.redrawOnDrag) {
redrawCanvas(layerdict);
}
layerdict.anotherPointerTapped = false;
}
delete layerdict.pointerStates[e.pointerId];
}
function handlePointerMove(e, layerdict) {
if (!layerdict.pointerStates.hasOwnProperty(e.pointerId)) {
return;
}
e.preventDefault();
e.stopPropagation();
if (!e.hasOwnProperty("offsetX")) {
// The polyfill doesn't set this properly
e.offsetX = e.pageX - e.currentTarget.offsetLeft;
e.offsetY = e.pageY - e.currentTarget.offsetTop;
}
var thisPtr = layerdict.pointerStates[e.pointerId];
var dx = e.offsetX - thisPtr.lastX;
var dy = e.offsetY - thisPtr.lastY;
// If this number is low on pointer up, we count the action as a click
thisPtr.distanceTravelled += Math.abs(dx) + Math.abs(dy);
if (Object.keys(layerdict.pointerStates).length == 1) {
// This is a simple drag
layerdict.transform.panx += devicePixelRatio * dx / layerdict.transform.zoom;
layerdict.transform.pany += devicePixelRatio * dy / layerdict.transform.zoom;
} else if (Object.keys(layerdict.pointerStates).length == 2) {
var otherPtr = Object.values(layerdict.pointerStates).filter((ptr) => ptr != thisPtr)[0];
var oldDist = Math.sqrt(Math.pow(thisPtr.lastX - otherPtr.lastX, 2) + Math.pow(thisPtr.lastY - otherPtr.lastY, 2));
var newDist = Math.sqrt(Math.pow(e.offsetX - otherPtr.lastX, 2) + Math.pow(e.offsetY - otherPtr.lastY, 2));
var scaleFactor = newDist / oldDist;
if (scaleFactor != NaN) {
layerdict.transform.zoom *= scaleFactor;
var zoomd = (1 - scaleFactor) / layerdict.transform.zoom;
layerdict.transform.panx += devicePixelRatio * otherPtr.lastX * zoomd;
layerdict.transform.pany += devicePixelRatio * otherPtr.lastY * zoomd;
}
}
thisPtr.lastX = e.offsetX;
thisPtr.lastY = e.offsetY;
if (settings.redrawOnDrag) {
redrawCanvas(layerdict);
}
}
function handleMouseWheel(e, layerdict) {
e.preventDefault();
e.stopPropagation();
var t = layerdict.transform;
var wheeldelta = e.deltaY;
if (e.deltaMode == 1) {
// FF only, scroll by lines
wheeldelta *= 30;
} else if (e.deltaMode == 2) {
wheeldelta *= 300;
}
var m = Math.pow(1.1, -wheeldelta / 40);
// Limit amount of zoom per tick.
if (m > 2) {
m = 2;
} else if (m < 0.5) {
m = 0.5;
}
t.zoom *= m;
var zoomd = (1 - m) / t.zoom;
t.panx += devicePixelRatio * e.offsetX * zoomd;
t.pany += devicePixelRatio * e.offsetY * zoomd;
redrawCanvas(layerdict);
}
function addMouseHandlers(div, layerdict) {
div.addEventListener("pointerdown", function(e) {
handlePointerDown(e, layerdict);
});
div.addEventListener("pointermove", function(e) {
handlePointerMove(e, layerdict);
});
div.addEventListener("pointerup", function(e) {
handlePointerUp(e, layerdict);
});
var pointerleave = function(e) {
handlePointerLeave(e, layerdict);
}
div.addEventListener("pointercancel", pointerleave);
div.addEventListener("pointerleave", pointerleave);
div.addEventListener("pointerout", pointerleave);
div.onwheel = function(e) {
handleMouseWheel(e, layerdict);
}
for (var element of [div, layerdict.bg, layerdict.fab, layerdict.silk, layerdict.highlight]) {
element.addEventListener("contextmenu", function(e) {
e.preventDefault();
}, false);
}
}
function setRedrawOnDrag(value) {
settings.redrawOnDrag = value;
writeStorage("redrawOnDrag", value);
}
function setBoardRotation(value) {
settings.boardRotation = value * 5;
writeStorage("boardRotation", settings.boardRotation);
document.getElementById("rotationDegree").textContent = settings.boardRotation;
resizeAll();
}
function initRender() {
allcanvas = {
front: {
transform: {
x: 0,
y: 0,
s: 1,
panx: 0,
pany: 0,
zoom: 1,
},
pointerStates: {},
anotherPointerTapped: false,
bg: document.getElementById("F_bg"),
fab: document.getElementById("F_fab"),
silk: document.getElementById("F_slk"),
highlight: document.getElementById("F_hl"),
layer: "F",
},
back: {
transform: {
x: 0,
y: 0,
s: 1,
panx: 0,
pany: 0,
zoom: 1,
},
pointerStates: {},
anotherPointerTapped: false,
bg: document.getElementById("B_bg"),
fab: document.getElementById("B_fab"),
silk: document.getElementById("B_slk"),
highlight: document.getElementById("B_hl"),
layer: "B",
}
};
addMouseHandlers(document.getElementById("frontcanvas"), allcanvas.front);
addMouseHandlers(document.getElementById("backcanvas"), allcanvas.back);
}
///////////////////////////////////////////////
///////////////////////////////////////////////
/*
* Table reordering via Drag'n'Drop
* Inspired by: https://htmldom.dev/drag-and-drop-table-column
*/
function setBomHandlers() {
const bom = document.getElementById('bomtable');
let dragName;
let placeHolderElements;
let draggingElement;
let forcePopulation;
let xOffset;
let yOffset;
let wasDragged;
const mouseUpHandler = function(e) {
// Delete dragging element
draggingElement.remove();
// Make BOM selectable again
bom.style.removeProperty("userSelect");
// Remove listeners
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('mouseup', mouseUpHandler);
if (wasDragged) {
// Redraw whole BOM
populateBomTable();
}
}
const mouseMoveHandler = function(e) {
// Notice the dragging
wasDragged = true;
// Make the dragged element visible
draggingElement.style.removeProperty("display");
// Set elements position to mouse position
draggingElement.style.left = `${e.screenX - xOffset}px`;
draggingElement.style.top = `${e.screenY - yOffset}px`;
// Forced redrawing of BOM table
if (forcePopulation) {
forcePopulation = false;
// Copy array
phe = Array.from(placeHolderElements);
// populate BOM table again
populateBomHeader(dragName, phe);
populateBomBody(dragName, phe);
}
// Set up array of hidden columns
var hiddenColumns = Array.from(settings.hiddenColumns);
// In the ungrouped mode, quantity don't exist
if (settings.bommode === "ungrouped")
hiddenColumns.push("Qty");
// If no checkbox fields can be found, we consider them hidden
if (settings.checkboxes.length == 0)
hiddenColumns.push("checkboxes");
// Get table headers and group them into checkboxes, extrafields and normal headers
const bh = document.getElementById("bomhead");
headers = Array.from(bh.querySelectorAll("th"))
headers.shift() // numCol is not part of the columnOrder
headerGroups = []
lastCompoundClass = null;
for (i = 0; i < settings.columnOrder.length; i++) {
cElem = settings.columnOrder[i];
if (hiddenColumns.includes(cElem)) {
// Hidden columns appear as a dummy element
headerGroups.push([]);
continue;
}
elem = headers.filter(e => getColumnOrderName(e) === cElem)[0];
if (elem.classList.contains("bom-checkbox")) {
if (lastCompoundClass === "bom-checkbox") {
cbGroup = headerGroups.pop();
cbGroup.push(elem);
headerGroups.push(cbGroup);
} else {
lastCompoundClass = "bom-checkbox";
headerGroups.push([elem])
}
} else {
headerGroups.push([elem])
}
}
// Copy settings.columnOrder
var columns = Array.from(settings.columnOrder)
// Set up array with indices of hidden columns
var hiddenIndices = hiddenColumns.map(e => settings.columnOrder.indexOf(e));
var dragIndex = columns.indexOf(dragName);
var swapIndex = dragIndex;
var swapDone = false;
// Check if the current dragged element is swapable with the left or right element
if (dragIndex > 0) {
// Get left headers boundingbox
swapIndex = dragIndex - 1;
while (hiddenIndices.includes(swapIndex) && swapIndex > 0)
swapIndex--;
if (!hiddenIndices.includes(swapIndex)) {
box = getBoundingClientRectFromMultiple(headerGroups[swapIndex]);
if (e.clientX < box.left + window.scrollX + (box.width / 2)) {
swapElement = columns[dragIndex];
columns.splice(dragIndex, 1);
columns.splice(swapIndex, 0, swapElement);
forcePopulation = true;
swapDone = true;
}
}
}
if ((!swapDone) && dragIndex < headerGroups.length - 1) {
// Get right headers boundingbox
swapIndex = dragIndex + 1;
while (hiddenIndices.includes(swapIndex))
swapIndex++;
if (swapIndex < headerGroups.length) {
box = getBoundingClientRectFromMultiple(headerGroups[swapIndex]);
if (e.clientX > box.left + window.scrollX + (box.width / 2)) {
swapElement = columns[dragIndex];
columns.splice(dragIndex, 1);
columns.splice(swapIndex, 0, swapElement);
forcePopulation = true;
swapDone = true;
}
}
}
// Write back change to storage
if (swapDone) {
settings.columnOrder = columns
writeStorage("columnOrder", JSON.stringify(columns));
}
}
const mouseDownHandler = function(e) {
var target = e.target;
if (target.tagName.toLowerCase() != "td")
target = target.parentElement;
// Used to check if a dragging has ever happened
wasDragged = false;
// Create new element which will be displayed as the dragged column
draggingElement = document.createElement("div")
draggingElement.classList.add("dragging");
draggingElement.style.display = "none";
draggingElement.style.position = "absolute";
draggingElement.style.overflow = "hidden";
// Get bomhead and bombody elements
const bh = document.getElementById("bomhead");
const bb = document.getElementById("bombody");
// Get all compound headers for the current column
var compoundHeaders;
if (target.classList.contains("bom-checkbox")) {
compoundHeaders = Array.from(bh.querySelectorAll("th.bom-checkbox"));
} else {
compoundHeaders = [target];
}
// Create new table which will display the column
var newTable = document.createElement("table");
newTable.classList.add("bom");
newTable.style.background = "white";
draggingElement.append(newTable);
// Create new header element
var newHeader = document.createElement("thead");
newTable.append(newHeader);
// Set up array for storing all placeholder elements
placeHolderElements = [];
// Add all compound headers to the new thead element and placeholders
compoundHeaders.forEach(function(h) {
clone = cloneElementWithDimensions(h);
newHeader.append(clone);
placeHolderElements.push(clone);
});
// Create new body element
var newBody = document.createElement("tbody");
newTable.append(newBody);
// Get indices for compound headers
var idxs = compoundHeaders.map(e => getBomTableHeaderIndex(e));
// For each row in the BOM body...
var rows = bb.querySelectorAll("tr");
rows.forEach(function(row) {
// ..get the cells for the compound column
const tds = row.querySelectorAll("td");
var copytds = idxs.map(i => tds[i]);
// Add them to the new element and the placeholders
var newRow = document.createElement("tr");
copytds.forEach(function(td) {
clone = cloneElementWithDimensions(td);
newRow.append(clone);
placeHolderElements.push(clone);
});
newBody.append(newRow);
});
// Compute width for compound header
var width = compoundHeaders.reduce((acc, x) => acc + x.clientWidth, 0);
draggingElement.style.width = `${width}px`;
// Insert the new dragging element and disable selection on BOM
bom.insertBefore(draggingElement, null);
bom.style.userSelect = "none";
// Determine the mouse position offset
xOffset = e.screenX - compoundHeaders.reduce((acc, x) => Math.min(acc, x.offsetLeft), compoundHeaders[0].offsetLeft);
yOffset = e.screenY - compoundHeaders[0].offsetTop;
// Get name for the column in settings.columnOrder
dragName = getColumnOrderName(target);
// Change text and class for placeholder elements
placeHolderElements = placeHolderElements.map(function(e) {
newElem = cloneElementWithDimensions(e);
newElem.textContent = "";
newElem.classList.add("placeholder");
return newElem;
});
// On next mouse move, the whole BOM needs to be redrawn to show the placeholders
forcePopulation = true;
// Add listeners for move and up on mouse
document.addEventListener('mousemove', mouseMoveHandler);
document.addEventListener('mouseup', mouseUpHandler);
}
// In netlist mode, there is nothing to reorder
if (settings.bommode === "netlist")
return;
// Add mouseDownHandler to every column except the numCol
bom.querySelectorAll("th")
.forEach(function(head) {
if (!head.classList.contains("numCol")) {
head.onmousedown = mouseDownHandler;
}
});
}
function getBoundingClientRectFromMultiple(elements) {
var elems = Array.from(elements);
if (elems.length == 0)
return null;
var box = elems.shift()
.getBoundingClientRect();
elems.forEach(function(elem) {
var elembox = elem.getBoundingClientRect();
box.left = Math.min(elembox.left, box.left);
box.top = Math.min(elembox.top, box.top);
box.width += elembox.width;
box.height = Math.max(elembox.height, box.height);
});
return box;
}
function cloneElementWithDimensions(elem) {
var newElem = elem.cloneNode(true);
newElem.style.height = window.getComputedStyle(elem).height;
newElem.style.width = window.getComputedStyle(elem).width;
return newElem;
}
function getBomTableHeaderIndex(elem) {
const bh = document.getElementById('bomhead');
const ths = Array.from(bh.querySelectorAll("th"));
return ths.indexOf(elem);
}
function getColumnOrderName(elem) {
var cname = elem.getAttribute("col_name");
if (cname === "bom-checkbox")
return "checkboxes";
else
return cname;
}
function resizableGrid(tablehead) {
var cols = tablehead.firstElementChild.children;
var rowWidth = tablehead.offsetWidth;
for (var i = 1; i < cols.length; i++) {
if (cols[i].classList.contains("bom-checkbox"))
continue;
cols[i].style.width = ((cols[i].clientWidth - paddingDiff(cols[i])) * 100 / rowWidth) + '%';
}
for (var i = 1; i < cols.length - 1; i++) {
var div = document.createElement('div');
div.className = "column-width-handle";
cols[i].appendChild(div);
setListeners(div);
}
function setListeners(div) {
var startX, curCol, nxtCol, curColWidth, nxtColWidth, rowWidth;
div.addEventListener('mousedown', function(e) {
e.preventDefault();
e.stopPropagation();
curCol = e.target.parentElement;
nxtCol = curCol.nextElementSibling;
startX = e.pageX;
var padding = paddingDiff(curCol);
rowWidth = curCol.parentElement.offsetWidth;
curColWidth = curCol.clientWidth - padding;
nxtColWidth = nxtCol.clientWidth - padding;
});
document.addEventListener('mousemove', function(e) {
if (startX) {
var diffX = e.pageX - startX;
diffX = -Math.min(-diffX, curColWidth - 20);
diffX = Math.min(diffX, nxtColWidth - 20);
curCol.style.width = ((curColWidth + diffX) * 100 / rowWidth) + '%';
nxtCol.style.width = ((nxtColWidth - diffX) * 100 / rowWidth) + '%';
console.log(`${curColWidth + nxtColWidth} ${(curColWidth + diffX) * 100 / rowWidth + (nxtColWidth - diffX) * 100 / rowWidth}`);
}
});
document.addEventListener('mouseup', function(e) {
curCol = undefined;
nxtCol = undefined;
startX = undefined;
nxtColWidth = undefined;
curColWidth = undefined
});
}
function paddingDiff(col) {
if (getStyleVal(col, 'box-sizing') == 'border-box') {
return 0;
}
var padLeft = getStyleVal(col, 'padding-left');
var padRight = getStyleVal(col, 'padding-right');
return (parseInt(padLeft) + parseInt(padRight));
}
function getStyleVal(elm, css) {
return (window.getComputedStyle(elm, null).getPropertyValue(css))
}
}
///////////////////////////////////////////////
///////////////////////////////////////////////
/* DOM manipulation and misc code */
var bomsplit;
var canvassplit;
var initDone = false;
var bomSortFunction = null;
var currentSortColumn = null;
var currentSortOrder = null;
var currentHighlightedRowId;
var highlightHandlers = [];
var footprintIndexToHandler = {};
var netsToHandler = {};
var markedFootprints = new Set();
var highlightedFootprints = [];
var highlightedNet = null;
var lastClicked;
function dbg(html) {
dbgdiv.innerHTML = html;
}
function redrawIfInitDone() {
if (initDone) {
redrawCanvas(allcanvas.front);
redrawCanvas(allcanvas.back);
}
}
function padsVisible(value) {
writeStorage("padsVisible", value);
settings.renderPads = value;
redrawIfInitDone();
}
function referencesVisible(value) {
writeStorage("referencesVisible", value);
settings.renderReferences = value;
redrawIfInitDone();
}
function valuesVisible(value) {
writeStorage("valuesVisible", value);
settings.renderValues = value;
redrawIfInitDone();
}
function tracksVisible(value) {
writeStorage("tracksVisible", value);
settings.renderTracks = value;
redrawIfInitDone();
}
function zonesVisible(value) {
writeStorage("zonesVisible", value);
settings.renderZones = value;
redrawIfInitDone();
}
function dnpOutline(value) {
writeStorage("dnpOutline", value);
settings.renderDnpOutline = value;
redrawIfInitDone();
}
function setDarkMode(value) {
if (value) {
topmostdiv.classList.add("dark");
} else {
topmostdiv.classList.remove("dark");
}
writeStorage("darkmode", value);
settings.darkMode = value;
redrawIfInitDone();
}
function setShowBOMColumn(field, value) {
if (field === "references") {
var rl = document.getElementById("reflookup");
rl.disabled = !value;
if (!value) {
rl.value = "";
updateRefLookup("");
}
}
var n = settings.hiddenColumns.indexOf(field);
if (value) {
if (n != -1) {
settings.hiddenColumns.splice(n, 1);
}
} else {
if (n == -1) {
settings.hiddenColumns.push(field);
}
}
writeStorage("hiddenColumns", JSON.stringify(settings.hiddenColumns));
if (initDone) {
populateBomTable();
}
redrawIfInitDone();
}
function setFullscreen(value) {
if (value) {
document.documentElement.requestFullscreen();
} else {
document.exitFullscreen();
}
}
function fabricationVisible(value) {
writeStorage("fabricationVisible", value);
settings.renderFabrication = value;
redrawIfInitDone();
}
function silkscreenVisible(value) {
writeStorage("silkscreenVisible", value);
settings.renderSilkscreen = value;
redrawIfInitDone();
}
function setHighlightPin1(value) {
writeStorage("highlightpin1", value);
settings.highlightpin1 = value;
redrawIfInitDone();
}
function getStoredCheckboxRefs(checkbox) {
function convert(ref) {
var intref = parseInt(ref);
if (isNaN(intref)) {
for (var i = 0; i < pcbdata.footprints.length; i++) {
if (pcbdata.footprints[i].ref == ref) {
return i;
}
}
return -1;
} else {
return intref;
}
}
if (!(checkbox in settings.checkboxStoredRefs)) {
var val = readStorage("checkbox_" + checkbox);
settings.checkboxStoredRefs[checkbox] = val ? val : "";
}
if (!settings.checkboxStoredRefs[checkbox]) {
return new Set();
} else {
return new Set(settings.checkboxStoredRefs[checkbox].split(",").map(r => convert(r)).filter(a => a >= 0));
}
}
function getCheckboxState(checkbox, references) {
var storedRefsSet = getStoredCheckboxRefs(checkbox);
var currentRefsSet = new Set(references.map(r => r[1]));
// Get difference of current - stored
var difference = new Set(currentRefsSet);
for (ref of storedRefsSet) {
difference.delete(ref);
}
if (difference.size == 0) {
// All the current refs are stored
return "checked";
} else if (difference.size == currentRefsSet.size) {
// None of the current refs are stored
return "unchecked";
} else {
// Some of the refs are stored
return "indeterminate";
}
}
function setBomCheckboxState(checkbox, element, references) {
var state = getCheckboxState(checkbox, references);
element.checked = (state == "checked");
element.indeterminate = (state == "indeterminate");
}
function createCheckboxChangeHandler(checkbox, references, row) {
return function() {
refsSet = getStoredCheckboxRefs(checkbox);
var markWhenChecked = settings.markWhenChecked == checkbox;
eventArgs = {
checkbox: checkbox,
refs: references,
}
if (this.checked) {
// checkbox ticked
for (var ref of references) {
refsSet.add(ref[1]);
}
if (markWhenChecked) {
row.classList.add("checked");
for (var ref of references) {
markedFootprints.add(ref[1]);
}
drawHighlights();
}
eventArgs.state = 'checked';
} else {
// checkbox unticked
for (var ref of references) {
refsSet.delete(ref[1]);
}
if (markWhenChecked) {
row.classList.remove("checked");
for (var ref of references) {
markedFootprints.delete(ref[1]);
}
drawHighlights();
}
eventArgs.state = 'unchecked';
}
settings.checkboxStoredRefs[checkbox] = [...refsSet].join(",");
writeStorage("checkbox_" + checkbox, settings.checkboxStoredRefs[checkbox]);
updateCheckboxStats(checkbox);
EventHandler.emitEvent(IBOM_EVENT_TYPES.CHECKBOX_CHANGE_EVENT, eventArgs);
}
}
function clearHighlightedFootprints() {
if (currentHighlightedRowId) {
document.getElementById(currentHighlightedRowId).classList.remove("highlighted");
currentHighlightedRowId = null;
highlightedFootprints = [];
highlightedNet = null;
}
}
function createRowHighlightHandler(rowid, refs, net) {
return function() {
if (currentHighlightedRowId) {
if (currentHighlightedRowId == rowid) {
return;
}
document.getElementById(currentHighlightedRowId).classList.remove("highlighted");
}
document.getElementById(rowid).classList.add("highlighted");
currentHighlightedRowId = rowid;
highlightedFootprints = refs ? refs.map(r => r[1]) : [];
highlightedNet = net;
drawHighlights();
EventHandler.emitEvent(
IBOM_EVENT_TYPES.HIGHLIGHT_EVENT, {
rowid: rowid,
refs: refs,
net: net
});
}
}
function entryMatches(entry) {
if (settings.bommode == "netlist") {
// entry is just a net name
return entry.toLowerCase().indexOf(filter) >= 0;
}
// check refs
if (!settings.hiddenColumns.includes("references")) {
for (var ref of entry[3]) {
if (ref[0].toLowerCase().indexOf(filter) >= 0) {
return true;
}
}
}
// check extra fields
if (!settings.hiddenColumns.includes("extrafields")) {
for (var i in config.extra_fields) {
if (entry[4][i].toLowerCase().indexOf(filter) >= 0) {
return true;
}
}
}
// check value
if (!settings.hiddenColumns.includes("value")) {
if (entry[1].toLowerCase().indexOf(filter) >= 0) {
return true;
}
}
// check footprint
if (!settings.hiddenColumns.includes("footprint")) {
if (entry[2].toLowerCase().indexOf(filter) >= 0) {
return true;
}
}
return false;
}
function findRefInEntry(entry) {
return entry[3].filter(r => r[0].toLowerCase().startsWith(reflookup));
}
function highlightFilter(s) {
if (!filter) {
return s;
}
var parts = s.toLowerCase().split(filter);
if (parts.length == 1) {
return s;
}
var r = "";
var pos = 0;
for (var i in parts) {
if (i > 0) {
r += '<mark class="highlight">' +
s.substring(pos, pos + filter.length) +
'</mark>';
pos += filter.length;
}
r += s.substring(pos, pos + parts[i].length);
pos += parts[i].length;
}
return r;
}
function checkboxSetUnsetAllHandler(checkboxname) {
return function() {
var checkboxnum = 0;
while (checkboxnum < settings.checkboxes.length &&
settings.checkboxes[checkboxnum].toLowerCase() != checkboxname.toLowerCase()) {
checkboxnum++;
}
if (checkboxnum >= settings.checkboxes.length) {
return;
}
var allset = true;
var checkbox;
var row;
for (row of bombody.childNodes) {
checkbox = row.childNodes[checkboxnum + 1].childNodes[0];
if (!checkbox.checked || checkbox.indeterminate) {
allset = false;
break;
}
}
for (row of bombody.childNodes) {
checkbox = row.childNodes[checkboxnum + 1].childNodes[0];
checkbox.checked = !allset;
checkbox.indeterminate = false;
checkbox.onchange();
}
}
}
function createColumnHeader(name, cls, comparator, is_checkbox = false) {
var th = document.createElement("TH");
th.innerHTML = name;
th.classList.add(cls);
if (is_checkbox)
th.setAttribute("col_name", "bom-checkbox");
else
th.setAttribute("col_name", name);
var span = document.createElement("SPAN");
span.classList.add("sortmark");
span.classList.add("none");
th.appendChild(span);
var spacer = document.createElement("div");
spacer.className = "column-spacer";
th.appendChild(spacer);
if (name=="References") //Sorting by column References
{
bomSortFunction = comparator;
currentSortColumn = th;
currentSortColumn.childNodes[1].classList.remove("none");
currentSortColumn.childNodes[1].classList.add("asc");
currentSortOrder = "asc";
}
spacer.onclick = function() {
if (currentSortColumn && th !== currentSortColumn) {
// Currently sorted by another column
currentSortColumn.childNodes[1].classList.remove(currentSortOrder);
currentSortColumn.childNodes[1].classList.add("none");
currentSortColumn = null;
currentSortOrder = null;
}
if (currentSortColumn && th === currentSortColumn) {
// Already sorted by this column
if (currentSortOrder == "asc") {
// Sort by this column, descending order
bomSortFunction = function(a, b) {
return -comparator(a, b);
}
currentSortColumn.childNodes[1].classList.remove("asc");
currentSortColumn.childNodes[1].classList.add("desc");
currentSortOrder = "desc";
} else {
// Unsort
bomSortFunction = null;
currentSortColumn.childNodes[1].classList.remove("desc");
currentSortColumn.childNodes[1].classList.add("none");
currentSortColumn = null;
currentSortOrder = null;
}
} else {
// Sort by this column, ascending order
bomSortFunction = comparator;
currentSortColumn = th;
currentSortColumn.childNodes[1].classList.remove("none");
currentSortColumn.childNodes[1].classList.add("asc");
currentSortOrder = "asc";
}
populateBomBody();
}
if (is_checkbox) {
spacer.onclick = fancyDblClickHandler(
spacer, spacer.onclick, checkboxSetUnsetAllHandler(name));
}
return th;
}
function populateBomHeader(placeHolderColumn = null, placeHolderElements = null) {
while (bomhead.firstChild) {
bomhead.removeChild(bomhead.firstChild);
}
var tr = document.createElement("TR");
var th = document.createElement("TH");
th.classList.add("numCol");
var vismenu = document.createElement("div");
vismenu.id = "vismenu";
vismenu.classList.add("menu");
var visbutton = document.createElement("div");
visbutton.classList.add("visbtn");
visbutton.classList.add("hideonprint");
var viscontent = document.createElement("div");
viscontent.classList.add("menu-content");
viscontent.id = "vismenu-content";
settings.columnOrder.forEach(column => {
if (typeof column !== "string")
return;
// Skip empty columns
if (column === "checkboxes" && settings.checkboxes.length == 0)
return;
else if (column === "Qty" && settings.bommode == "ungrouped")
return;
var label = document.createElement("label");
label.classList.add("menu-label");
var input = document.createElement("input");
input.classList.add("visibility_checkbox");
input.type = "checkbox";
input.onchange = function(e) {
setShowBOMColumn(column, e.target.checked)
};
input.checked = !(settings.hiddenColumns.includes(column));
label.appendChild(input);
if (column.length > 0)
label.append(column[0].toUpperCase() + column.slice(1));
viscontent.appendChild(label);
});
viscontent.childNodes[0].classList.add("menu-label-top");
vismenu.appendChild(visbutton);
if (settings.bommode != "netlist") {
vismenu.appendChild(viscontent);
th.appendChild(vismenu);
}
tr.appendChild(th);
var checkboxCompareClosure = function(checkbox) {
return (a, b) => {
var stateA = getCheckboxState(checkbox, a[3]);
var stateB = getCheckboxState(checkbox, b[3]);
if (stateA > stateB) return -1;
if (stateA < stateB) return 1;
return 0;
}
}
if (settings.bommode == "netlist") {
th = createColumnHeader("Net name", "bom-netname", (a, b) => {
if (a > b) return -1;
if (a < b) return 1;
return 0;
});
tr.appendChild(th);
} else {
// Filter hidden columns
var columns = settings.columnOrder.filter(e => !settings.hiddenColumns.includes(e));
columns.forEach((column) => {
if (column === placeHolderColumn) {
var n = 1;
if (column === "checkboxes")
n = settings.checkboxes.length;
for (i = 0; i < n; i++) {
td = placeHolderElements.shift();
tr.appendChild(td);
}
return;
}
if (column === "checkboxes") {
for (var checkbox of settings.checkboxes) {
th = createColumnHeader(
checkbox, "bom-checkbox", checkboxCompareClosure(checkbox), true);
tr.appendChild(th);
}
}
if (column === "References") {
tr.appendChild(createColumnHeader("References", "references", (a, b) => {
var i = 0;
while (i < a[3].length && i < b[3].length) {
var compres = refCompare(a[3][i], b[3][i]);
if (compres!=0) return compres;
//if (a[3][i] != b[3][i]) return a[3][i] > b[3][i] ? 1 : -1;
i++;
}
return a[3].length - b[3].length;
}));
}
if (column === "Value") {
tr.appendChild(createColumnHeader("Value", "value", (a, b) => {
return valueCompare(a[5], b[5], a[1], b[1]);
}));
}
if (column === "Footprint") {
tr.appendChild(createColumnHeader("Footprint", "footprint", (a, b) => {
if (a[2] != b[2]) return a[2] > b[2] ? 1 : -1;
else return 0;
}));
}
if (column === "Qty" && settings.bommode == "grouped") {
tr.appendChild(createColumnHeader("Qty", "quantity", (a, b) => {
return a[3].length - b[3].length;
}));
}
// Extra fields
var extraFieldCompareClosure = function(fieldIndex) {
return (a, b) => {
var fa = a[4][fieldIndex];
var fb = b[4][fieldIndex];
if (fa != fb) return fa > fb ? 1 : -1;
else return 0;
}
}
var i = config.extra_fields.indexOf(column);
if (i < 0)
return;
tr.appendChild(createColumnHeader(
column, `extrafield${i+1}`, extraFieldCompareClosure(i)));
});
}
bomhead.appendChild(tr);
}
function populateBomBody(placeholderColumn = null, placeHolderElements = null) {
while (bom.firstChild) {
bom.removeChild(bom.firstChild);
}
highlightHandlers = [];
footprintIndexToHandler = {};
netsToHandler = {};
currentHighlightedRowId = null;
var first = true;
if (settings.bommode == "netlist") {
bomtable = pcbdata.nets.slice();
} else {
switch (settings.canvaslayout) {
case 'F':
bomtable = pcbdata.bom.F.slice();
break;
case 'FB':
bomtable = pcbdata.bom.both.slice();
break;
case 'B':
bomtable = pcbdata.bom.B.slice();
break;
}
if (settings.bommode == "ungrouped") {
// expand bom table
expandedTable = []
for (var bomentry of bomtable) {
for (var ref of bomentry[3]) {
expandedTable.push([1, bomentry[1], bomentry[2],
[ref], bomentry[4], bomentry[5]
]);
}
}
bomtable = expandedTable;
}
}
if (bomSortFunction) {
bomtable = bomtable.sort(bomSortFunction);
}
for (var i in bomtable) {
var bomentry = bomtable[i];
if (filter && !entryMatches(bomentry)) {
continue;
}
var references = null;
var netname = null;
var tr = document.createElement("TR");
var td = document.createElement("TD");
var rownum = +i + 1;
tr.id = "bomrow" + rownum;
td.textContent = rownum;
tr.appendChild(td);
if (settings.bommode == "netlist") {
netname = bomentry;
td = document.createElement("TD");
td.innerHTML = highlightFilter(netname ? netname : "&lt;no net&gt;");
tr.appendChild(td);
} else {
if (reflookup) {
references = findRefInEntry(bomentry);
if (references.length == 0) {
continue;
}
} else {
references = bomentry[3];
}
// Filter hidden columns
var columns = settings.columnOrder.filter(e => !settings.hiddenColumns.includes(e));
columns.forEach((column) => {
if (column === placeholderColumn) {
var n = 1;
if (column === "checkboxes")
n = settings.checkboxes.length;
for (i = 0; i < n; i++) {
td = placeHolderElements.shift();
tr.appendChild(td);
}
return;
}
// Checkboxes
if (column === "checkboxes") {
for (var checkbox of settings.checkboxes) {
if (checkbox) {
td = document.createElement("TD");
var input = document.createElement("input");
input.type = "checkbox";
input.onchange = createCheckboxChangeHandler(checkbox, references, tr);
setBomCheckboxState(checkbox, input, references);
if (input.checked && settings.markWhenChecked == checkbox) {
tr.classList.add("checked");
}
td.appendChild(input);
tr.appendChild(td);
}
}
}
// References
if (column === "References") {
td = document.createElement("TD");
if (config.groupdes)
{
var groupedRefs = [];
references.map(r => {
var match = r[0].match(/^(.*?)([0-9]+)$/);
if (match) {
return [match[1], parseInt(match[2]), r[0]];
} else {
return [r[0], NaN, r[0]];
}
}).forEach(([prefix, num, r]) => {
if (groupedRefs.length > 0) {
var last = groupedRefs[groupedRefs.length-1];
if (last[0] === prefix && last[1] === num-1) {
last[1] = num;
last[3] = r;
return;
}
}
groupedRefs.push([prefix, num, r, r]);
});
td.innerHTML = highlightFilter(groupedRefs.map(([prefix, num, r1, r2]) => (r1 === r2 ? r1 : r1+"-"+r2)).join(", "));
}
else
{
td.innerHTML = highlightFilter(references.map(r => r[0]).join(", "));
}
tr.appendChild(td);
}
// Value
if (column === "Value") {
td = document.createElement("TD");
td.innerHTML = highlightFilter(bomentry[1]);
tr.appendChild(td);
}
// Footprint
if (column === "Footprint") {
td = document.createElement("TD");
td.innerHTML = highlightFilter(bomentry[2]);
tr.appendChild(td);
}
if (column === "Qty" && settings.bommode == "grouped") {
// Quantity
td = document.createElement("TD");
td.textContent = bomentry[3].length;
tr.appendChild(td);
}
// Extra fields
var i = config.extra_fields.indexOf(column)
if (i < 0)
return;
td = document.createElement("TD");
td.innerHTML = highlightFilter(bomentry[4][i]);
tr.appendChild(td);
});
}
bom.appendChild(tr);
var handler = createRowHighlightHandler(tr.id, references, netname);
tr.onmousemove = handler;
highlightHandlers.push({
id: tr.id,
handler: handler,
});
if (references !== null) {
for (var refIndex of references.map(r => r[1])) {
footprintIndexToHandler[refIndex] = handler;
}
}
if (netname !== null) {
netsToHandler[netname] = handler;
}
if ((filter || reflookup) && first) {
handler();
first = false;
}
}
EventHandler.emitEvent(
IBOM_EVENT_TYPES.BOM_BODY_CHANGE_EVENT, {
filter: filter,
reflookup: reflookup,
checkboxes: settings.checkboxes,
bommode: settings.bommode,
});
}
function highlightPreviousRow() {
if (!currentHighlightedRowId) {
highlightHandlers[highlightHandlers.length - 1].handler();
} else {
if (highlightHandlers.length > 1 &&
highlightHandlers[0].id == currentHighlightedRowId) {
highlightHandlers[highlightHandlers.length - 1].handler();
} else {
for (var i = 0; i < highlightHandlers.length - 1; i++) {
if (highlightHandlers[i + 1].id == currentHighlightedRowId) {
highlightHandlers[i].handler();
break;
}
}
}
}
smoothScrollToRow(currentHighlightedRowId);
}
function highlightNextRow() {
if (!currentHighlightedRowId) {
highlightHandlers[0].handler();
} else {
if (highlightHandlers.length > 1 &&
highlightHandlers[highlightHandlers.length - 1].id == currentHighlightedRowId) {
highlightHandlers[0].handler();
} else {
for (var i = 1; i < highlightHandlers.length; i++) {
if (highlightHandlers[i - 1].id == currentHighlightedRowId) {
highlightHandlers[i].handler();
break;
}
}
}
}
smoothScrollToRow(currentHighlightedRowId);
}
function populateBomTable() {
populateBomHeader();
populateBomBody();
setBomHandlers();
resizableGrid(bomhead);
}
function footprintsClicked(footprintIndexes) {
var lastClickedIndex = footprintIndexes.indexOf(lastClicked);
for (var i = 1; i <= footprintIndexes.length; i++) {
var refIndex = footprintIndexes[(lastClickedIndex + i) % footprintIndexes.length];
if (refIndex in footprintIndexToHandler) {
lastClicked = refIndex;
footprintIndexToHandler[refIndex]();
smoothScrollToRow(currentHighlightedRowId);
break;
}
}
}
function netClicked(net) {
if (net in netsToHandler) {
netsToHandler[net]();
smoothScrollToRow(currentHighlightedRowId);
} else {
clearHighlightedFootprints();
highlightedNet = net;
drawHighlights();
}
}
function updateFilter(input) {
filter = input.toLowerCase();
populateBomTable();
}
function updateRefLookup(input) {
reflookup = input.toLowerCase();
populateBomTable();
}
function changeCanvasLayout(layout) {
document.getElementById("fl-btn").classList.remove("depressed");
document.getElementById("fb-btn").classList.remove("depressed");
document.getElementById("bl-btn").classList.remove("depressed");
switch (layout) {
case 'F':
document.getElementById("fl-btn").classList.add("depressed");
if (settings.bomlayout != "bom-only") {
canvassplit.collapse(1);
}
break;
case 'B':
document.getElementById("bl-btn").classList.add("depressed");
if (settings.bomlayout != "bom-only") {
canvassplit.collapse(0);
}
break;
default:
document.getElementById("fb-btn").classList.add("depressed");
if (settings.bomlayout != "bom-only") {
canvassplit.setSizes([50, 50]);
}
}
settings.canvaslayout = layout;
writeStorage("canvaslayout", layout);
resizeAll();
changeBomMode(settings.bommode);
}
function populateMetadata() {
document.getElementById("title").innerHTML = pcbdata.metadata.title;
document.getElementById("revision").innerHTML = pcbdata.metadata.revision;
document.getElementById("company").innerHTML = pcbdata.metadata.company;
document.getElementById("filedate").innerHTML = pcbdata.metadata.date;
if (pcbdata.metadata.title != "") {
document.title = pcbdata.metadata.title + " BOM";
}
// Calculate board stats
var fp_f = 0,
fp_b = 0,
pads_f = 0,
pads_b = 0,
pads_th = 0;
nets = 0;
if ("nets" in pcbdata) {
nets = pcbdata.nets.length;
}
for (var i = 0; i < pcbdata.footprints.length; i++) {
if (pcbdata.bom.skipped.includes(i)) continue;
var mod = pcbdata.footprints[i];
if (mod.ref=="") continue;
if (mod.layer == "F") {
fp_f++;
} else {
fp_b++;
}
for (var pad of mod.pads) {
if (pad.type == "th") {
pads_th++;
} else {
if (pad.layers.includes("F")) {
pads_f++;
}
if (pad.layers.includes("B")) {
pads_b++;
}
}
}
}
document.getElementById("stats-components-front").innerHTML = fp_f;
document.getElementById("stats-components-back").innerHTML = fp_b;
document.getElementById("stats-components-total").innerHTML = fp_f + fp_b;
document.getElementById("stats-groups-front").innerHTML = pcbdata.bom.F.length;
document.getElementById("stats-groups-back").innerHTML = pcbdata.bom.B.length;
document.getElementById("stats-groups-total").innerHTML = pcbdata.bom.both.length;
document.getElementById("stats-smd-pads-front").innerHTML = pads_f;
document.getElementById("stats-smd-pads-back").innerHTML = pads_b;
document.getElementById("stats-smd-pads-total").innerHTML = pads_f + pads_b;
document.getElementById("stats-th-pads").innerHTML = pads_th;
document.getElementById("stats-nets").innerHTML = nets;
// Update version string
document.getElementById("github-link").innerHTML = "InteractiveHtmlBom&nbsp;" +
/^v\d+\.\d+/.exec(pcbdata.ibom_version)[0];
}
function changeBomLayout(layout) {
document.getElementById("bom-btn").classList.remove("depressed");
document.getElementById("lr-btn").classList.remove("depressed");
document.getElementById("tb-btn").classList.remove("depressed");
switch (layout) {
case 'bom-only':
document.getElementById("bom-btn").classList.add("depressed");
if (bomsplit) {
bomsplit.destroy();
bomsplit = null;
canvassplit.destroy();
canvassplit = null;
}
document.getElementById("frontcanvas").style.display = "none";
document.getElementById("backcanvas").style.display = "none";
document.getElementById("bot").style.height = "";
break;
case 'top-bottom':
document.getElementById("tb-btn").classList.add("depressed");
document.getElementById("frontcanvas").style.display = "";
document.getElementById("backcanvas").style.display = "";
document.getElementById("bot").style.height = "calc(100% - 80px)";
document.getElementById("bomdiv").classList.remove("split-horizontal");
document.getElementById("canvasdiv").classList.remove("split-horizontal");
document.getElementById("frontcanvas").classList.add("split-horizontal");
document.getElementById("backcanvas").classList.add("split-horizontal");
if (bomsplit) {
bomsplit.destroy();
bomsplit = null;
canvassplit.destroy();
canvassplit = null;
}
bomsplit = Split(['#bomdiv', '#canvasdiv'], {
sizes: [50, 50],
onDrag: resizeAll,
direction: "vertical",
gutterSize: 5
});
canvassplit = Split(['#frontcanvas', '#backcanvas'], {
sizes: [50, 50],
gutterSize: 5,
onDrag: resizeAll
});
break;
case 'left-right':
document.getElementById("lr-btn").classList.add("depressed");
document.getElementById("frontcanvas").style.display = "";
document.getElementById("backcanvas").style.display = "";
document.getElementById("bot").style.height = "calc(100% - 80px)";
document.getElementById("bomdiv").classList.add("split-horizontal");
document.getElementById("canvasdiv").classList.add("split-horizontal");
document.getElementById("frontcanvas").classList.remove("split-horizontal");
document.getElementById("backcanvas").classList.remove("split-horizontal");
if (bomsplit) {
bomsplit.destroy();
bomsplit = null;
canvassplit.destroy();
canvassplit = null;
}
bomsplit = Split(['#bomdiv', '#canvasdiv'], {
sizes: [50, 50],
onDrag: resizeAll,
gutterSize: 5
});
canvassplit = Split(['#frontcanvas', '#backcanvas'], {
sizes: [50, 50],
gutterSize: 5,
direction: "vertical",
onDrag: resizeAll
});
}
settings.bomlayout = layout;
writeStorage("bomlayout", layout);
changeCanvasLayout(settings.canvaslayout);
}
function changeBomMode(mode) {
document.getElementById("bom-grouped-btn").classList.remove("depressed");
document.getElementById("bom-ungrouped-btn").classList.remove("depressed");
document.getElementById("bom-netlist-btn").classList.remove("depressed");
var chkbxs = document.getElementsByClassName("visibility_checkbox");
switch (mode) {
case 'grouped':
document.getElementById("bom-grouped-btn").classList.add("depressed");
for (var i = 0; i < chkbxs.length; i++) {
chkbxs[i].disabled = false;
}
break;
case 'ungrouped':
document.getElementById("bom-ungrouped-btn").classList.add("depressed");
for (var i = 0; i < chkbxs.length; i++) {
chkbxs[i].disabled = false;
}
break;
case 'netlist':
document.getElementById("bom-netlist-btn").classList.add("depressed");
for (var i = 0; i < chkbxs.length; i++) {
chkbxs[i].disabled = true;
}
}
writeStorage("bommode", mode);
if (mode != settings.bommode) {
settings.bommode = mode;
bomSortFunction = null;
currentSortColumn = null;
currentSortOrder = null;
clearHighlightedFootprints();
}
populateBomTable();
}
function focusFilterField() {
focusInputField(document.getElementById("filter"));
}
function focusRefLookupField() {
focusInputField(document.getElementById("reflookup"));
}
function toggleBomCheckbox(bomrowid, checkboxnum) {
if (!bomrowid || checkboxnum > settings.checkboxes.length) {
return;
}
var bomrow = document.getElementById(bomrowid);
var checkbox = bomrow.childNodes[checkboxnum].childNodes[0];
checkbox.checked = !checkbox.checked;
checkbox.indeterminate = false;
checkbox.onchange();
}
function checkBomCheckbox(bomrowid, checkboxname) {
var checkboxnum = 0;
while (checkboxnum < settings.checkboxes.length &&
settings.checkboxes[checkboxnum].toLowerCase() != checkboxname.toLowerCase()) {
checkboxnum++;
}
if (!bomrowid || checkboxnum >= settings.checkboxes.length) {
return;
}
var bomrow = document.getElementById(bomrowid);
var checkbox = bomrow.childNodes[checkboxnum + 1].childNodes[0];
checkbox.checked = true;
checkbox.indeterminate = false;
checkbox.onchange();
}
function setBomCheckboxes(value) {
writeStorage("bomCheckboxes", value);
settings.checkboxes = value.split(",").map((e) => e.trim()).filter((e) => e);
prepCheckboxes();
populateMarkWhenCheckedOptions();
setMarkWhenChecked(settings.markWhenChecked);
}
function setMarkWhenChecked(value) {
writeStorage("markWhenChecked", value);
settings.markWhenChecked = value;
markedFootprints.clear();
for (var ref of (value ? getStoredCheckboxRefs(value) : [])) {
markedFootprints.add(ref);
}
populateBomTable();
drawHighlights();
}
function prepCheckboxes() {
var table = document.getElementById("checkbox-stats");
while (table.childElementCount > 1) {
table.removeChild(table.lastChild);
}
if (settings.checkboxes.length) {
table.style.display = "";
} else {
table.style.display = "none";
}
for (var checkbox of settings.checkboxes) {
var tr = document.createElement("TR");
var td = document.createElement("TD");
td.innerHTML = checkbox;
tr.appendChild(td);
td = document.createElement("TD");
td.id = "checkbox-stats-" + checkbox;
var progressbar = document.createElement("div");
progressbar.classList.add("bar");
td.appendChild(progressbar);
var text = document.createElement("div");
text.classList.add("text");
td.appendChild(text);
tr.appendChild(td);
table.appendChild(tr);
updateCheckboxStats(checkbox);
}
}
function populateMarkWhenCheckedOptions() {
var container = document.getElementById("markWhenCheckedContainer");
if (settings.checkboxes.length == 0) {
container.parentElement.style.display = "none";
return;
}
container.innerHTML = '';
container.parentElement.style.display = "inline-block";
function createOption(name, displayName) {
var id = "markWhenChecked-" + name;
var div = document.createElement("div");
div.classList.add("radio-container");
var input = document.createElement("input");
input.type = "radio";
input.name = "markWhenChecked";
input.value = name;
input.id = id;
input.onchange = () => setMarkWhenChecked(name);
div.appendChild(input);
// Preserve the selected element when the checkboxes change
if (name == settings.markWhenChecked) {
input.checked = true;
}
var label = document.createElement("label");
label.innerHTML = displayName;
label.htmlFor = id;
div.appendChild(label);
container.appendChild(div);
}
createOption("", "None");
for (var checkbox of settings.checkboxes) {
createOption(checkbox, checkbox);
}
}
function updateCheckboxStats(checkbox) {
var checked = getStoredCheckboxRefs(checkbox).size;
var total = pcbdata.footprints.length - pcbdata.bom.skipped.length;
for (var i = 0; i < pcbdata.footprints.length; i++) {
var mod = pcbdata.footprints[i];
if (mod.ref == "") total--;
}
var percent = checked * 100.0 / total;
var td = document.getElementById("checkbox-stats-" + checkbox);
td.firstChild.style.width = percent + "%";
td.lastChild.innerHTML = checked + "/" + total + " (" + Math.round(percent) + "%)";
}
function resetSettings ()
{
storage.clear();
location.reload();
}
document.onkeydown = function(e) {
switch (e.key) {
case "n":
if (document.activeElement.type == "text") {
return;
}
if (currentHighlightedRowId !== null) {
checkBomCheckbox(currentHighlightedRowId, "placed");
highlightNextRow();
e.preventDefault();
}
break;
case "ArrowUp":
highlightPreviousRow();
e.preventDefault();
break;
case "ArrowDown":
highlightNextRow();
e.preventDefault();
break;
default:
break;
}
if (e.altKey) {
switch (e.key) {
case "f":
focusFilterField();
e.preventDefault();
break;
case "r":
focusRefLookupField();
e.preventDefault();
break;
case "z":
changeBomLayout("bom-only");
e.preventDefault();
break;
case "x":
changeBomLayout("left-right");
e.preventDefault();
break;
case "c":
changeBomLayout("top-bottom");
e.preventDefault();
break;
case "v":
changeCanvasLayout("F");
e.preventDefault();
break;
case "b":
changeCanvasLayout("FB");
e.preventDefault();
break;
case "n":
changeCanvasLayout("B");
e.preventDefault();
break;
default:
break;
}
if (e.key >= '1' && e.key <= '9') {
toggleBomCheckbox(currentHighlightedRowId, parseInt(e.key));
e.preventDefault();
}
}
}
function hideNetlistButton() {
document.getElementById("bom-ungrouped-btn").classList.remove("middle-button");
document.getElementById("bom-ungrouped-btn").classList.add("right-most-button");
document.getElementById("bom-netlist-btn").style.display = "none";
}
window.onload = function(e) {
initUtils();
initRender();
initStorage();
initDefaults();
cleanGutters();
populateMetadata();
dbgdiv = document.getElementById("dbg");
bom = document.getElementById("bombody");
bomhead = document.getElementById("bomhead");
filter = "";
reflookup = "";
if (!("nets" in pcbdata)) {
hideNetlistButton();
}
initDone = true;
setBomCheckboxes(document.getElementById("bomCheckboxes").value);
// Triggers render
changeBomLayout(settings.bomlayout);
// Users may leave fullscreen without touching the checkbox. Uncheck.
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement)
document.getElementById('fullscreenCheckbox').checked = false;
});
}
window.onresize = resizeAll;
window.matchMedia("print").addListener(resizeAll);
///////////////////////////////////////////////
///////////////////////////////////////////////
EventHandler.registerCallback(IBOM_EVENT_TYPES.BOM_BODY_CHANGE_EVENT, () => {
for(var tr of bom.childNodes) {
tr.onclick = tr.onmousemove;
tr.onmousemove = null;
};
});
///////////////////////////////////////////////
</script>
</head>
<body>
<div id="topmostdiv" class="topmostdiv">
<div id="top">
<div style="float: right; height: 100%;">
<div class="hideonprint menu" style="float: right; top: 8px;">
<button class="menubtn"></button>
<div class="menu-content">
<label class="menu-label menu-label-top" style="width: calc(50% - 18px)">
<input id="darkmodeCheckbox" type="checkbox" onchange="setDarkMode(this.checked)">
Dark mode
</label><!-- This comment eats space! All of it!
--><label class="menu-label menu-label-top" style="width: calc(50% - 17px); border-left: 0;">
<input id="fullscreenCheckbox" type="checkbox" onchange="setFullscreen(this.checked)">
Full Screen
</label>
<label class="menu-label" style="width: calc(50% - 18px)">
<input id="fabricationCheckbox" type="checkbox" checked onchange="fabricationVisible(this.checked)">
Fab layer
</label><!-- This comment eats space! All of it!
--><label class="menu-label" style="width: calc(50% - 17px); border-left: 0;">
<input id="silkscreenCheckbox" type="checkbox" checked onchange="silkscreenVisible(this.checked)">
Silkscreen
</label>
<label class="menu-label" style="width: calc(50% - 18px)">
<input id="referencesCheckbox" type="checkbox" checked onchange="referencesVisible(this.checked)">
Designators
</label><!-- This comment eats space! All of it!
--><label class="menu-label" style="width: calc(50% - 17px); border-left: 0;">
<input id="valuesCheckbox" type="checkbox" checked onchange="valuesVisible(this.checked)">
Values
</label>
<div id="tracksAndZonesCheckboxes">
<label class="menu-label" style="width: calc(50% - 18px)">
<input id="tracksCheckbox" type="checkbox" checked onchange="tracksVisible(this.checked)">
Tracks
</label><!-- This comment eats space! All of it!
--><label class="menu-label" style="width: calc(50% - 17px); border-left: 0;">
<input id="zonesCheckbox" type="checkbox" checked onchange="zonesVisible(this.checked)">
Zones
</label>
</div>
<label class="menu-label" style="width: calc(50% - 18px)">
<input id="padsCheckbox" type="checkbox" checked onchange="padsVisible(this.checked)">
Pads
</label><!-- This comment eats space! All of it!
--><label class="menu-label" style="width: calc(50% - 17px); border-left: 0;">
<input id="dnpOutlineCheckbox" type="checkbox" checked onchange="dnpOutline(this.checked)">
DNP outlined
</label>
<label class="menu-label">
<input id="highlightpin1Checkbox" type="checkbox" onchange="setHighlightPin1(this.checked)">
Highlight first pin
</label>
<label class="menu-label">
<input id="dragCheckbox" type="checkbox" checked onchange="setRedrawOnDrag(this.checked)">
Continuous redraw on drag
</label>
<label class="menu-label">
<span>Board rotation</span>
<span style="float: right"><span id="rotationDegree">0</span>&#176;</span>
<input id="boardRotation" type="range" min="-36" max="36" value="0" class="slider" oninput="setBoardRotation(this.value)">
</label>
<label class="menu-label">
<div style="margin-left: 5px">Bom checkboxes</div>
<input id="bomCheckboxes" class="menu-textbox" type=text
oninput="setBomCheckboxes(this.value)">
</label>
<label class="menu-label">
<div style="margin-left: 5px">Mark when checked</div>
<div id="markWhenCheckedContainer"></div>
</label>
<label class="menu-label">
<span class="shameless-plug">
<span>Created using</span>
<a id="github-link" target="blank" href="https://github.com/openscopeproject/InteractiveHtmlBom">InteractiveHtmlBom</a>
</span>
<button class="savebtn" onclick="resetSettings()">Reset settings</button>
</label>
</div>
</div>
<div class="button-container hideonprint"
style="float: right; position: relative; top: 8px">
<button id="fl-btn" class="left-most-button" onclick="changeCanvasLayout('F')"
title="Front only">F
</button>
<button id="fb-btn" class="middle-button" onclick="changeCanvasLayout('FB')"
title="Front and Back">FB
</button>
<button id="bl-btn" class="right-most-button" onclick="changeCanvasLayout('B')"
title="Back only">B
</button>
</div>
<div class="button-container hideonprint"
style="float: right; position: relative; top: 8px">
<button id="bom-btn" class="left-most-button" onclick="changeBomLayout('bom-only')"
title="BOM only"></button>
<button id="lr-btn" class="middle-button" onclick="changeBomLayout('left-right')"
title="BOM left, drawings right"></button>
<button id="tb-btn" class="right-most-button" onclick="changeBomLayout('top-bottom')"
title="BOM top, drawings bot"></button>
</div>
<div class="button-container hideonprint"
style="float: right; position: relative; top: 8px">
<button id="bom-grouped-btn" class="left-most-button" onclick="changeBomMode('grouped')"
title="Grouped BOM"></button>
<button id="bom-ungrouped-btn" class="middle-button" onclick="changeBomMode('ungrouped')"
title="Ungrouped BOM"></button>
<button id="bom-netlist-btn" class="right-most-button" onclick="changeBomMode('netlist')"
title="Netlist"></button>
</div>
<div class="hideonprint menu" style="float: right; top: 8px;">
<button class="statsbtn"></button>
<div class="menu-content">
<table class="stats">
<tbody>
<tr>
<td width="40%">Board stats</td>
<td>Front</td>
<td>Back</td>
<td>Total</td>
</tr>
<tr>
<td>Components</td>
<td id="stats-components-front">~</td>
<td id="stats-components-back">~</td>
<td id="stats-components-total">~</td>
</tr>
<tr>
<td>Groups</td>
<td id="stats-groups-front">~</td>
<td id="stats-groups-back">~</td>
<td id="stats-groups-total">~</td>
</tr>
<tr>
<td>SMD pads</td>
<td id="stats-smd-pads-front">~</td>
<td id="stats-smd-pads-back">~</td>
<td id="stats-smd-pads-total">~</td>
</tr>
<tr>
<td>TH pads</td>
<td colspan=3 id="stats-th-pads">~</td>
</tr>
<tr>
<td>Nets</td>
<td colspan=3 id="stats-nets">~</td>
</tr>
</tbody>
</table>
<table class="stats">
<col width="40%"/><col />
<tbody id="checkbox-stats">
<tr>
<td colspan=2 style="border-top: 0">Checkboxes</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="hideonprint menu" style="float: right; top: 8px;">
<button class="iobtn"></button>
<div class="menu-content">
<div class="menu-label menu-label-top">
<div style="margin-left: 5px;">Save board image</div>
<div class="flexbox">
<input id="render-save-width" class="menu-textbox" type="text" value="1000" placeholder="Width"
style="flex-grow: 1; width: 50px;" oninput="validateSaveImgDimension(this)">
<span>X</span>
<input id="render-save-height" class="menu-textbox" type="text" value="1000" placeholder="Height"
style="flex-grow: 1; width: 50px;" oninput="validateSaveImgDimension(this)">
</div>
<label>
<input id="render-save-transparent" type="checkbox">
Transparent background
</label>
<div class="flexbox">
<button class="savebtn" onclick="saveImage('F')">Front</button>
<button class="savebtn" onclick="saveImage('B')">Back</button>
</div>
</div>
<div class="menu-label">
<span style="margin-left: 5px;">Config and checkbox state</span>
<div class="flexbox">
<button class="savebtn" onclick="saveSettings()">Export</button>
<button class="savebtn" onclick="loadSettings()">Import</button>
</div>
</div>
</div>
</div>
</div>
<div id="fileinfodiv" style="overflow: auto;">
<table class="fileinfo">
<tbody>
<tr>
<td id="title" class="title" style="width: 70%">
Title
</td>
<td id="revision" class="title" style="width: 30%">
Revision
</td>
</tr>
<tr>
<td id="company">
Company
</td>
<td id="filedate">
Date
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="bot" class="split" style="height: calc(100% - 80px)">
<div id="bomdiv" class="split split-horizontal">
<div style="width: 100%">
<!-- <div style="width: 100%; position: sticky; position: -webkit-sticky; top: 0px ; height: 60px; background-color: inherit; z-index: 10;">-->
<input id="reflookup" class="textbox searchbox reflookup hideonprint" type="text" placeholder="Ref lookup"
oninput="updateRefLookup(this.value)">
<input id="filter" class="textbox searchbox filter hideonprint" type="text" placeholder="Filter"
oninput="updateFilter(this.value)">
<div class="button-container hideonprint" style="float: left; margin: 0;">
<button id="copy" title="Copy bom table to clipboard"
onclick="copyToClipboard()"></button>
</div>
</div>
<div id="dbg"></div>
<table class="bom" id="bomtable" >
<thead id="bomhead" style="position: sticky; position: -webkit-sticky; top: 0px; z-index: 10">
<!-- <thead id="bomhead" style="position: sticky; position: -webkit-sticky; top: 57px; background-color: inherit; z-index: 10">-->
</thead>
<tbody id="bombody">
</tbody>
</table>
</div>
<div id="canvasdiv" class="split split-horizontal">
<div id="frontcanvas" class="split" touch-action="none" style="overflow: hidden">
<div style="position: relative; width: 100%; height: 100%;">
<canvas id="F_bg" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
<canvas id="F_fab" style="position: absolute; left: 0; top: 0; z-index: 1;"></canvas>
<canvas id="F_slk" style="position: absolute; left: 0; top: 0; z-index: 2;"></canvas>
<canvas id="F_hl" style="position: absolute; left: 0; top: 0; z-index: 3;"></canvas>
</div>
</div>
<div id="backcanvas" class="split" touch-action="none" style="overflow: hidden">
<div style="position: relative; width: 100%; height: 100%;">
<canvas id="B_bg" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
<canvas id="B_fab" style="position: absolute; left: 0; top: 0; z-index: 1;"></canvas>
<canvas id="B_slk" style="position: absolute; left: 0; top: 0; z-index: 2;"></canvas>
<canvas id="B_hl" style="position: absolute; left: 0; top: 0; z-index: 3;"></canvas>
</div>
</div>
</div>
</div>
</div>
</body>
</html>