4443 lines
212 KiB
HTML
4443 lines
212 KiB
HTML
<!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 KiCAD</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: 30px;
|
|
}
|
|
|
|
.bom .value {
|
|
width: 15%;
|
|
}
|
|
|
|
.bom .quantity {
|
|
width: 65px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.help-link {
|
|
border: 1px solid #0278a4;
|
|
padding-inline: 0.3rem;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.dark .help-link {
|
|
border: 1px solid #00b9fd;
|
|
}
|
|
|
|
|
|
</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.onDragEnd&&c.onDragEnd(),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 = {"dark_mode": true, "show_pads": true, "show_fabrication": false, "show_silkscreen": true, "highlight_pin1": true, "redraw_on_drag": true, "board_rotation": 0, "checkboxes": "Sourced,Placed", "bom_view": "left-right", "layer_view": "FB", "offset_back_rotation": false, "kicad_text_formatting": true, "fields": ["Value", "Footprint", "Manufacturer_Name", "Manufacturer_Part_Number", "Mouser Part Number"]}
|
|
///////////////////////////////////////////////
|
|
|
|
///////////////////////////////////////////////
|
|
var pcbdata = JSON.parse(LZString.decompressFromBase64("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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 saveBomTable(output) {
|
|
var text = '';
|
|
for (var node of bomhead.childNodes[0].childNodes) {
|
|
if (node.firstChild) {
|
|
text += (output == 'csv' ? `"${node.firstChild.nodeValue}"` : node.firstChild.nodeValue);
|
|
}
|
|
if (node != bomhead.childNodes[0].lastChild) {
|
|
text += (output == 'csv' ? ',' : '\t');
|
|
}
|
|
}
|
|
text += '\n';
|
|
for (var row of bombody.childNodes) {
|
|
for (var cell of row.childNodes) {
|
|
let val = '';
|
|
for (var node of cell.childNodes) {
|
|
if (node.nodeName == "INPUT") {
|
|
if (node.checked) {
|
|
val += '✓';
|
|
}
|
|
} else if ((node.nodeName == "MARK") || (node.nodeName == "A")) {
|
|
val += node.firstChild.nodeValue;
|
|
} else {
|
|
val += node.nodeValue;
|
|
}
|
|
}
|
|
if (output == 'csv') {
|
|
val = val.replace(/\"/g, '\"\"'); // pair of double-quote characters
|
|
if (isNumeric(val)) {
|
|
val = +val; // use number
|
|
} else {
|
|
val = `"${val}"`; // enclosed within double-quote
|
|
}
|
|
}
|
|
text += val;
|
|
if (cell != row.lastChild) {
|
|
text += (output == 'csv' ? ',' : '\t');
|
|
}
|
|
}
|
|
text += '\n';
|
|
}
|
|
|
|
if (output != 'clipboard') {
|
|
// To file: csv or txt
|
|
var blob = new Blob([text], {
|
|
type: `text/${output}`
|
|
});
|
|
saveFile(`${pcbdata.metadata.title}.${output}`, blob);
|
|
} else {
|
|
// To clipboard
|
|
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 isNumeric(str) {
|
|
/* https://stackoverflow.com/a/175787 */
|
|
return (typeof str != "string" ? false : !isNaN(str) && !isNaN(parseFloat(str)));
|
|
}
|
|
|
|
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.*)?$", "");
|
|
if (config.fields.includes("Value")) {
|
|
var index = config.fields.indexOf("Value");
|
|
pcbdata.bom["parsedValues"] = {};
|
|
for (var id in pcbdata.bom.fields) {
|
|
pcbdata.bom.parsedValues[id] = parseValue(pcbdata.bom.fields[id][index])
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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;
|
|
writeStorage("boardRotation", settings.boardRotation);
|
|
document.getElementById("boardRotation").value = settings.boardRotation / 5;
|
|
document.getElementById("rotationDegree").textContent = settings.boardRotation;
|
|
setOffsetBackRotation(settings.offsetBackRotation);
|
|
document.getElementById("offsetBackRotationCheckbox").checked = settings.offsetBackRotation;
|
|
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,
|
|
offsetBackRotation: false,
|
|
renderPads: true,
|
|
renderReferences: true,
|
|
renderValues: true,
|
|
renderSilkscreen: true,
|
|
renderFabrication: true,
|
|
renderDnpOutline: false,
|
|
renderTracks: true,
|
|
renderZones: true,
|
|
columnOrder: [],
|
|
hiddenColumns: []
|
|
}
|
|
|
|
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", "References"].concat(config.fields).concat(["Quantity"]);
|
|
var hcols = JSON.parse(readStorage("hiddenColumns"));
|
|
if (hcols === null) {
|
|
hcols = [];
|
|
}
|
|
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;
|
|
initBooleanSetting("offsetBackRotation", config.offset_back_rotation, "offsetBackRotationCheckbox", setOffsetBackRotation);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
if ("polygons" in text) {
|
|
ctx.fill(getPolygonsPath(text));
|
|
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 (config.kicad_text_formatting) {
|
|
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) {
|
|
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) {
|
|
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.angle));
|
|
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.lineWidth = 3 / scalefactor;
|
|
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);
|
|
}
|
|
}
|
|
ctx.lineWidth = 3 / scalefactor;
|
|
// 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'),
|
|
}
|
|
|
|
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.center,
|
|
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, zone.fillrule || "nonzero");
|
|
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);
|
|
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 + (flip && settings.offsetBackRotation ? - 180 : 0)));
|
|
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, flip) {
|
|
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 + (flip && settings.offsetBackRotation ? - 180 : 0)));
|
|
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 flip = (layerdict.layer === "B");
|
|
var bbox = applyRotation(pcbdata.edges_bbox, flip);
|
|
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;
|
|
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.center, 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;
|
|
var flip = layerdict.layer === "B";
|
|
if (flip) {
|
|
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 + (flip && settings.offsetBackRotation ? - 180 : 0));
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 setOffsetBackRotation(value) {
|
|
settings.offsetBackRotation = value;
|
|
writeStorage("offsetBackRotation", value);
|
|
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("Quantity");
|
|
// 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) {
|
|
if (ref[0].toLowerCase().indexOf(filter) >= 0) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
// check fields
|
|
for (var i in config.fields) {
|
|
var f = config.fields[i];
|
|
if (!settings.hiddenColumns.includes(f)) {
|
|
for (var ref of entry) {
|
|
if (pcbdata.bom.fields[ref[1]][i].toLowerCase().indexOf(filter) >= 0) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function findRefInEntry(entry) {
|
|
return entry.filter(r => r[0].toLowerCase() == 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);
|
|
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 === "Quantity" && 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);
|
|
var stateB = getCheckboxState(checkbox, b);
|
|
if (stateA > stateB) return -1;
|
|
if (stateA < stateB) return 1;
|
|
return 0;
|
|
}
|
|
}
|
|
var stringFieldCompareClosure = function (fieldIndex) {
|
|
return (a, b) => {
|
|
var fa = pcbdata.bom.fields[a[0][1]][fieldIndex];
|
|
var fb = pcbdata.bom.fields[b[0][1]][fieldIndex];
|
|
if (fa != fb) return fa > fb ? 1 : -1;
|
|
else return 0;
|
|
}
|
|
}
|
|
var referenceRegex = /(?<prefix>[^0-9]+)(?<number>[0-9]+)/;
|
|
var compareRefs = (a, b) => {
|
|
var ra = referenceRegex.exec(a);
|
|
var rb = referenceRegex.exec(b);
|
|
if (ra === null || rb === null) {
|
|
if (a != b) return a > b ? 1 : -1;
|
|
return 0;
|
|
} else {
|
|
if (ra.groups.prefix != rb.groups.prefix) {
|
|
return ra.groups.prefix > rb.groups.prefix ? 1 : -1;
|
|
}
|
|
if (ra.groups.number != rb.groups.number) {
|
|
return parseInt(ra.groups.number) > parseInt(rb.groups.number) ? 1 : -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));
|
|
var valueIndex = config.fields.indexOf("Value");
|
|
var footprintIndex = config.fields.indexOf("Footprint");
|
|
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;
|
|
} else if (column === "checkboxes") {
|
|
for (var checkbox of settings.checkboxes) {
|
|
th = createColumnHeader(
|
|
checkbox, "bom-checkbox", checkboxCompareClosure(checkbox), true);
|
|
tr.appendChild(th);
|
|
}
|
|
} else if (column === "References") {
|
|
tr.appendChild(createColumnHeader("References", "references", (a, b) => {
|
|
var i = 0;
|
|
while (i < a.length && i < b.length) {
|
|
if (a[i] != b[i]) return compareRefs(a[i][0], b[i][0]);
|
|
i++;
|
|
}
|
|
return a.length - b.length;
|
|
}));
|
|
} else if (column === "Value") {
|
|
tr.appendChild(createColumnHeader("Value", "value", (a, b) => {
|
|
var ra = a[0][1], rb = b[0][1];
|
|
return valueCompare(
|
|
pcbdata.bom.parsedValues[ra], pcbdata.bom.parsedValues[rb],
|
|
pcbdata.bom.fields[ra][valueIndex], pcbdata.bom.fields[rb][valueIndex]);
|
|
}));
|
|
return;
|
|
} else if (column === "Footprint") {
|
|
tr.appendChild(createColumnHeader(
|
|
"Footprint", "footprint", stringFieldCompareClosure(footprintIndex)));
|
|
} else if (column === "Quantity" && settings.bommode == "grouped") {
|
|
tr.appendChild(createColumnHeader("Quantity", "quantity", (a, b) => {
|
|
return a.length - b.length;
|
|
}));
|
|
} else {
|
|
// Other fields
|
|
var i = config.fields.indexOf(column);
|
|
if (i < 0)
|
|
return;
|
|
tr.appendChild(createColumnHeader(
|
|
column, `field${i + 1}`, stringFieldCompareClosure(i)));
|
|
}
|
|
});
|
|
}
|
|
bomhead.appendChild(tr);
|
|
}
|
|
|
|
function populateBomBody(placeholderColumn = null, placeHolderElements = null) {
|
|
const urlRegex = /^(https?:\/\/[^\s\/$.?#][^\s]*|file:\/\/([a-zA-Z]:|\/)[^\x00]+)$/;
|
|
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) {
|
|
expandedTable.push([ref]);
|
|
}
|
|
}
|
|
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 : "<no net>");
|
|
tr.appendChild(td);
|
|
} else {
|
|
if (reflookup) {
|
|
references = findRefInEntry(bomentry);
|
|
if (references.length == 0) {
|
|
continue;
|
|
}
|
|
} else {
|
|
references = bomentry;
|
|
}
|
|
// 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;
|
|
} else 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);
|
|
}
|
|
}
|
|
} else if (column === "References") {
|
|
td = document.createElement("TD");
|
|
td.innerHTML = highlightFilter(references.map(r => r[0]).join(", "));
|
|
tr.appendChild(td);
|
|
} else if (column === "Quantity" && settings.bommode == "grouped") {
|
|
// Quantity
|
|
td = document.createElement("TD");
|
|
td.textContent = references.length;
|
|
tr.appendChild(td);
|
|
} else {
|
|
// All the other fields
|
|
var field_index = config.fields.indexOf(column)
|
|
if (field_index < 0)
|
|
return;
|
|
var valueSet = new Set();
|
|
references.map(r => r[1]).forEach((id) => valueSet.add(pcbdata.bom.fields[id][field_index]));
|
|
td = document.createElement("TD");
|
|
var output = new Array();
|
|
for (let item of valueSet) {
|
|
const visible = highlightFilter(item);
|
|
if (typeof item === 'string' && item.match(urlRegex)) {
|
|
output.push(`<a href="${item}" target="_blank">${visible}</a>`);
|
|
} else {
|
|
output.push(visible);
|
|
}
|
|
}
|
|
td.innerHTML = output.join(", ");
|
|
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 = "Rev: " + 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;
|
|
for (var i = 0; i < pcbdata.footprints.length; i++) {
|
|
if (pcbdata.bom.skipped.includes(i)) continue;
|
|
var mod = pcbdata.footprints[i];
|
|
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;
|
|
// Update version string
|
|
document.getElementById("github-link").innerHTML = "InteractiveHtmlBom " +
|
|
/^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],
|
|
onDragEnd: resizeAll,
|
|
direction: "vertical",
|
|
gutterSize: 5
|
|
});
|
|
canvassplit = Split(['#frontcanvas', '#backcanvas'], {
|
|
sizes: [50, 50],
|
|
gutterSize: 5,
|
|
onDragEnd: 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],
|
|
onDragEnd: resizeAll,
|
|
gutterSize: 5
|
|
});
|
|
canvassplit = Split(['#frontcanvas', '#backcanvas'], {
|
|
sizes: [50, 50],
|
|
gutterSize: 5,
|
|
direction: "vertical",
|
|
onDragEnd: 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;
|
|
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 constrain(number, min, max){
|
|
return Math.min(Math.max(parseInt(number), min), max);
|
|
}
|
|
|
|
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;
|
|
case "ArrowLeft":
|
|
case "ArrowRight":
|
|
if (document.activeElement.type != "text"){
|
|
e.preventDefault();
|
|
let boardRotationElement = document.getElementById("boardRotation")
|
|
settings.boardRotation = parseInt(boardRotationElement.value); // degrees / 5
|
|
if (e.key == "ArrowLeft"){
|
|
settings.boardRotation += 3; // 15 degrees
|
|
}
|
|
else{
|
|
settings.boardRotation -= 3;
|
|
}
|
|
settings.boardRotation = constrain(settings.boardRotation, boardRotationElement.min, boardRotationElement.max);
|
|
boardRotationElement.value = settings.boardRotation
|
|
setBoardRotation(settings.boardRotation);
|
|
}
|
|
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);
|
|
|
|
///////////////////////////////////////////////
|
|
|
|
///////////////////////////////////////////////
|
|
|
|
///////////////////////////////////////////////
|
|
</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)">
|
|
References
|
|
</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>°</span>
|
|
<input id="boardRotation" type="range" min="-36" max="36" value="0" class="slider" oninput="setBoardRotation(this.value)">
|
|
</label>
|
|
<label class="menu-label">
|
|
<input id="offsetBackRotationCheckbox" type="checkbox" onchange="setOffsetBackRotation(this.checked)">
|
|
Offset back rotation
|
|
</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>
|
|
<a target="blank" title="Mouse and keyboard help" href="https://github.com/openscopeproject/InteractiveHtmlBom/wiki/Usage#bom-page-mouse-actions" style="text-decoration: none;"><label class="help-link">?</label></a>
|
|
</span>
|
|
</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>
|
|
</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 class="menu-label">
|
|
<span style="margin-left: 5px;">Save bom table as</span>
|
|
<div class="flexbox">
|
|
<button class="savebtn" onclick="saveBomTable('csv')">csv</button>
|
|
<button class="savebtn" onclick="saveBomTable('txt')">txt</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%">
|
|
<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="saveBomTable('clipboard')"></button>
|
|
</div>
|
|
</div>
|
|
<div id="dbg"></div>
|
|
<table class="bom" id="bomtable">
|
|
<thead id="bomhead">
|
|
</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>
|