source: trunk/partlist/ibom.html@ 30

Last change on this file since 30 was 30, checked in by Zed, 6 weeks ago
File size: 196.0 KB
RevLine 
[30]1<!DOCTYPE html>
2<html lang="en">
3
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <title>Interactive BOM for KiCAD</title>
8 <style type="text/css">
9:root {
10 --pcb-edge-color: black;
11 --pad-color: #878787;
12 --pad-hole-color: #CCCCCC;
13 --pad-color-highlight: #D04040;
14 --pad-color-highlight-both: #D0D040;
15 --pad-color-highlight-marked: #44a344;
16 --pin1-outline-color: #ffb629;
17 --pin1-outline-color-highlight: #ffb629;
18 --pin1-outline-color-highlight-both: #fcbb39;
19 --pin1-outline-color-highlight-marked: #fdbe41;
20 --silkscreen-edge-color: #aa4;
21 --silkscreen-polygon-color: #4aa;
22 --silkscreen-text-color: #4aa;
23 --fabrication-edge-color: #907651;
24 --fabrication-polygon-color: #907651;
25 --fabrication-text-color: #a27c24;
26 --track-color: #def5f1;
27 --track-color-highlight: #D04040;
28 --zone-color: #def5f1;
29 --zone-color-highlight: #d0404080;
30}
31
32html,
33body {
34 margin: 0px;
35 height: 100%;
36 font-family: Verdana, sans-serif;
37}
38
39.dark.topmostdiv {
40 --pcb-edge-color: #eee;
41 --pad-color: #808080;
42 --pin1-outline-color: #ffa800;
43 --pin1-outline-color-highlight: #ccff00;
44 --track-color: #42524f;
45 --zone-color: #42524f;
46 background-color: #252c30;
47 color: #eee;
48}
49
50button {
51 background-color: #eee;
52 border: 1px solid #888;
53 color: black;
54 height: 44px;
55 width: 44px;
56 text-align: center;
57 text-decoration: none;
58 display: inline-block;
59 font-size: 14px;
60 font-weight: bolder;
61}
62
63.dark button {
64 /* This will be inverted */
65 background-color: #c3b7b5;
66}
67
68button.depressed {
69 background-color: #0a0;
70 color: white;
71}
72
73.dark button.depressed {
74 /* This will be inverted */
75 background-color: #b3b;
76}
77
78button:focus {
79 outline: 0;
80}
81
82button#tb-btn {
83 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");
84}
85
86button#lr-btn {
87 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");
88}
89
90button#bom-btn {
91 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");
92}
93
94button#bom-grouped-btn {
95 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");
96}
97
98button#bom-ungrouped-btn {
99 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");
100}
101
102button#bom-netlist-btn {
103 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");
104}
105
106button#copy {
107 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");
108 background-position: 6px 6px;
109 background-repeat: no-repeat;
110 background-size: 26px 26px;
111 border-radius: 6px;
112 height: 40px;
113 width: 40px;
114 margin: 10px 5px;
115}
116
117button#copy:active {
118 box-shadow: inset 0px 0px 5px #6c6c6c;
119}
120
121textarea.clipboard-temp {
122 position: fixed;
123 top: 0;
124 left: 0;
125 width: 2em;
126 height: 2em;
127 padding: 0;
128 border: None;
129 outline: None;
130 box-shadow: None;
131 background: transparent;
132}
133
134.left-most-button {
135 border-right: 0;
136 border-top-left-radius: 6px;
137 border-bottom-left-radius: 6px;
138}
139
140.middle-button {
141 border-right: 0;
142}
143
144.right-most-button {
145 border-top-right-radius: 6px;
146 border-bottom-right-radius: 6px;
147}
148
149.button-container {
150 font-size: 0;
151 margin: 0.4rem 0.4rem 0.4rem 0;
152}
153
154.dark .button-container {
155 filter: invert(1);
156}
157
158.button-container button {
159 background-size: 32px 32px;
160 background-position: 5px 5px;
161 background-repeat: no-repeat;
162}
163
164@media print {
165 .hideonprint {
166 display: none;
167 }
168}
169
170canvas {
171 cursor: crosshair;
172}
173
174canvas:active {
175 cursor: grabbing;
176}
177
178.fileinfo {
179 width: 100%;
180 max-width: 1000px;
181 border: none;
182 padding: 3px;
183}
184
185.fileinfo .title {
186 font-size: 20pt;
187 font-weight: bold;
188}
189
190.fileinfo td {
191 overflow: hidden;
192 white-space: nowrap;
193 max-width: 1px;
194 width: 50%;
195 text-overflow: ellipsis;
196}
197
198.bom {
199 border-collapse: collapse;
200 font-family: Consolas, "DejaVu Sans Mono", Monaco, monospace;
201 font-size: 10pt;
202 table-layout: fixed;
203 width: 100%;
204 margin-top: 1px;
205 position: relative;
206}
207
208.bom th,
209.bom td {
210 border: 1px solid black;
211 padding: 5px;
212 word-wrap: break-word;
213 text-align: center;
214 position: relative;
215}
216
217.dark .bom th,
218.dark .bom td {
219 border: 1px solid #777;
220}
221
222.bom th {
223 background-color: #CCCCCC;
224 background-clip: padding-box;
225}
226
227.dark .bom th {
228 background-color: #3b4749;
229}
230
231.bom tr.highlighted:nth-child(n) {
232 background-color: #cfc;
233}
234
235.dark .bom tr.highlighted:nth-child(n) {
236 background-color: #226022;
237}
238
239.bom tr:nth-child(even) {
240 background-color: #f2f2f2;
241}
242
243.dark .bom tr:nth-child(even) {
244 background-color: #313b40;
245}
246
247.bom tr.checked {
248 color: #1cb53d;
249}
250
251.dark .bom tr.checked {
252 color: #2cce54;
253}
254
255.bom tr {
256 transition: background-color 0.2s;
257}
258
259.bom .numCol {
260 width: 30px;
261}
262
263.bom .value {
264 width: 15%;
265}
266
267.bom .quantity {
268 width: 65px;
269}
270
271.bom th .sortmark {
272 position: absolute;
273 right: 1px;
274 top: 1px;
275 margin-top: -5px;
276 border-width: 5px;
277 border-style: solid;
278 border-color: transparent transparent #221 transparent;
279 transform-origin: 50% 85%;
280 transition: opacity 0.2s, transform 0.4s;
281}
282
283.dark .bom th .sortmark {
284 filter: invert(1);
285}
286
287.bom th .sortmark.none {
288 opacity: 0;
289}
290
291.bom th .sortmark.desc {
292 transform: rotate(180deg);
293}
294
295.bom th:hover .sortmark.none {
296 opacity: 0.5;
297}
298
299.bom .bom-checkbox {
300 width: 30px;
301 position: relative;
302 user-select: none;
303 -moz-user-select: none;
304}
305
306.bom .bom-checkbox:before {
307 content: "";
308 position: absolute;
309 border-width: 15px;
310 border-style: solid;
311 border-color: #51829f transparent transparent transparent;
312 visibility: hidden;
313 top: -15px;
314}
315
316.bom .bom-checkbox:after {
317 content: "Double click to set/unset all";
318 position: absolute;
319 color: white;
320 top: -35px;
321 left: -26px;
322 background: #51829f;
323 padding: 5px 15px;
324 border-radius: 8px;
325 white-space: nowrap;
326 visibility: hidden;
327}
328
329.bom .bom-checkbox:hover:before,
330.bom .bom-checkbox:hover:after {
331 visibility: visible;
332 transition: visibility 0.2s linear 1s;
333}
334
335.split {
336 -webkit-box-sizing: border-box;
337 -moz-box-sizing: border-box;
338 box-sizing: border-box;
339 overflow-y: auto;
340 overflow-x: hidden;
341 background-color: inherit;
342}
343
344.split.split-horizontal,
345.gutter.gutter-horizontal {
346 height: 100%;
347 float: left;
348}
349
350.gutter {
351 background-color: #ddd;
352 background-repeat: no-repeat;
353 background-position: 50%;
354 transition: background-color 0.3s;
355}
356
357.dark .gutter {
358 background-color: #777;
359}
360
361.gutter.gutter-horizontal {
362 background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
363 cursor: ew-resize;
364 width: 5px;
365}
366
367.gutter.gutter-vertical {
368 background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
369 cursor: ns-resize;
370 height: 5px;
371}
372
373.searchbox {
374 float: left;
375 height: 40px;
376 margin: 10px 5px;
377 padding: 12px 32px;
378 font-family: Consolas, "DejaVu Sans Mono", Monaco, monospace;
379 font-size: 18px;
380 box-sizing: border-box;
381 border: 1px solid #888;
382 border-radius: 6px;
383 outline: none;
384 background-color: #eee;
385 transition: background-color 0.2s, border 0.2s;
386 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==');
387 background-position: 10px 10px;
388 background-repeat: no-repeat;
389}
390
391.dark .searchbox {
392 background-color: #111;
393 color: #eee;
394}
395
396.searchbox::placeholder {
397 color: #ccc;
398}
399
400.dark .searchbox::placeholder {
401 color: #666;
402}
403
404.filter {
405 width: calc(60% - 64px);
406}
407
408.reflookup {
409 width: calc(40% - 10px);
410}
411
412input[type=text]:focus {
413 background-color: white;
414 border: 1px solid #333;
415}
416
417.dark input[type=text]:focus {
418 background-color: #333;
419 border: 1px solid #ccc;
420}
421
422mark.highlight {
423 background-color: #5050ff;
424 color: #fff;
425 padding: 2px;
426 border-radius: 6px;
427}
428
429.dark mark.highlight {
430 background-color: #76a6da;
431 color: #111;
432}
433
434.menubtn {
435 background-color: white;
436 border: none;
437 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");
438 background-position: center;
439 background-repeat: no-repeat;
440}
441
442.statsbtn {
443 background-color: white;
444 border: none;
445 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");
446 background-position: center;
447 background-repeat: no-repeat;
448}
449
450.iobtn {
451 background-color: white;
452 border: none;
453 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");
454 background-position: center;
455 background-repeat: no-repeat;
456}
457
458.visbtn {
459 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");
460 background-position: center;
461 background-repeat: no-repeat;
462 padding: 15px;
463}
464
465#vismenu-content {
466 left: 0px;
467 font-family: Verdana, sans-serif;
468}
469
470.dark .statsbtn,
471.dark .savebtn,
472.dark .menubtn,
473.dark .iobtn,
474.dark .visbtn {
475 filter: invert(1);
476}
477
478.flexbox {
479 display: flex;
480 align-items: center;
481 justify-content: space-between;
482 width: 100%;
483}
484
485.savebtn {
486 background-color: #d6d6d6;
487 width: auto;
488 height: 30px;
489 flex-grow: 1;
490 margin: 5px;
491 border-radius: 4px;
492}
493
494.savebtn:active {
495 background-color: #0a0;
496 color: white;
497}
498
499.dark .savebtn:active {
500 /* This will be inverted */
501 background-color: #b3b;
502}
503
504.stats {
505 border-collapse: collapse;
506 font-size: 12pt;
507 table-layout: fixed;
508 width: 100%;
509 min-width: 450px;
510}
511
512.dark .stats td {
513 border: 1px solid #bbb;
514}
515
516.stats td {
517 border: 1px solid black;
518 padding: 5px;
519 word-wrap: break-word;
520 text-align: center;
521 position: relative;
522}
523
524#checkbox-stats div {
525 position: absolute;
526 left: 0;
527 top: 0;
528 height: 100%;
529 width: 100%;
530 display: flex;
531 align-items: center;
532 justify-content: center;
533}
534
535#checkbox-stats .bar {
536 background-color: rgba(28, 251, 0, 0.6);
537}
538
539.menu {
540 position: relative;
541 display: inline-block;
542 margin: 0.4rem 0.4rem 0.4rem 0;
543}
544
545.menu-content {
546 font-size: 12pt !important;
547 text-align: left !important;
548 font-weight: normal !important;
549 display: none;
550 position: absolute;
551 background-color: white;
552 right: 0;
553 min-width: 300px;
554 box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
555 z-index: 100;
556 padding: 8px;
557}
558
559.dark .menu-content {
560 background-color: #111;
561}
562
563.menu:hover .menu-content {
564 display: block;
565}
566
567.menu:hover .menubtn,
568.menu:hover .iobtn,
569.menu:hover .statsbtn {
570 background-color: #eee;
571}
572
573.menu-label {
574 display: inline-block;
575 padding: 8px;
576 border: 1px solid #ccc;
577 border-top: 0;
578 width: calc(100% - 18px);
579}
580
581.menu-label-top {
582 border-top: 1px solid #ccc;
583}
584
585.menu-textbox {
586 float: left;
587 height: 24px;
588 margin: 10px 5px;
589 padding: 5px 5px;
590 font-family: Consolas, "DejaVu Sans Mono", Monaco, monospace;
591 font-size: 14px;
592 box-sizing: border-box;
593 border: 1px solid #888;
594 border-radius: 4px;
595 outline: none;
596 background-color: #eee;
597 transition: background-color 0.2s, border 0.2s;
598 width: calc(100% - 10px);
599}
600
601.menu-textbox.invalid,
602.dark .menu-textbox.invalid {
603 color: red;
604}
605
606.dark .menu-textbox {
607 background-color: #222;
608 color: #eee;
609}
610
611.radio-container {
612 margin: 4px;
613}
614
615.topmostdiv {
616 display: flex;
617 flex-direction: column;
618 width: 100%;
619 background-color: white;
620 transition: background-color 0.3s;
621 min-height: 100%;
622}
623
624#top {
625 display: flex;
626 flex-wrap: wrap;
627 justify-content: flex-end;
628 align-items: center;
629}
630
631#topdivider {
632 border-bottom: 2px solid black;
633 display: flex;
634 justify-content: center;
635 align-items: center;
636}
637
638.dark #topdivider {
639 border-bottom: 2px solid #ccc;
640}
641
642#topdivider>div {
643 position: relative;
644}
645
646#toptoggle {
647 cursor: pointer;
648 user-select: none;
649 position: absolute;
650 padding: 0.1rem 0.3rem;
651 top: -0.4rem;
652 left: -1rem;
653 font-size: 1.4rem;
654 line-height: 60%;
655 border: 1px solid black;
656 border-radius: 1rem;
657 background-color: #fff;
658 z-index: 100;
659}
660
661.flipped {
662 transform: rotate(0.5turn);
663}
664
665.dark #toptoggle {
666 border: 1px solid #fff;
667 background-color: #222;
668}
669
670#fileinfodiv {
671 flex: 20rem 1 0;
672 overflow: auto;
673}
674
675#bomcontrols {
676 display: flex;
677 flex-direction: row-reverse;
678}
679
680#bomcontrols>* {
681 flex-shrink: 0;
682}
683
684#dbg {
685 display: block;
686}
687
688::-webkit-scrollbar {
689 width: 8px;
690}
691
692::-webkit-scrollbar-track {
693 background: #aaa;
694}
695
696::-webkit-scrollbar-thumb {
697 background: #666;
698 border-radius: 3px;
699}
700
701::-webkit-scrollbar-thumb:hover {
702 background: #555;
703}
704
705.slider {
706 -webkit-appearance: none;
707 width: 100%;
708 margin: 3px 0;
709 padding: 0;
710 outline: none;
711 opacity: 0.7;
712 -webkit-transition: .2s;
713 transition: opacity .2s;
714 border-radius: 3px;
715}
716
717.slider:hover {
718 opacity: 1;
719}
720
721.slider:focus {
722 outline: none;
723}
724
725.slider::-webkit-slider-runnable-track {
726 -webkit-appearance: none;
727 width: 100%;
728 height: 8px;
729 background: #d3d3d3;
730 border-radius: 3px;
731 border: none;
732}
733
734.slider::-webkit-slider-thumb {
735 -webkit-appearance: none;
736 width: 15px;
737 height: 15px;
738 border-radius: 50%;
739 background: #0a0;
740 cursor: pointer;
741 margin-top: -4px;
742}
743
744.dark .slider::-webkit-slider-thumb {
745 background: #3d3;
746}
747
748.slider::-moz-range-thumb {
749 width: 15px;
750 height: 15px;
751 border-radius: 50%;
752 background: #0a0;
753 cursor: pointer;
754}
755
756.slider::-moz-range-track {
757 height: 8px;
758 background: #d3d3d3;
759 border-radius: 3px;
760}
761
762.dark .slider::-moz-range-thumb {
763 background: #3d3;
764}
765
766.slider::-ms-track {
767 width: 100%;
768 height: 8px;
769 border-width: 3px 0;
770 background: transparent;
771 border-color: transparent;
772 color: transparent;
773 transition: opacity .2s;
774}
775
776.slider::-ms-fill-lower {
777 background: #d3d3d3;
778 border: none;
779 border-radius: 3px;
780}
781
782.slider::-ms-fill-upper {
783 background: #d3d3d3;
784 border: none;
785 border-radius: 3px;
786}
787
788.slider::-ms-thumb {
789 width: 15px;
790 height: 15px;
791 border-radius: 50%;
792 background: #0a0;
793 cursor: pointer;
794 margin: 0;
795}
796
797.shameless-plug {
798 font-size: 0.8em;
799 text-align: center;
800 display: block;
801}
802
803a {
804 color: #0278a4;
805}
806
807.dark a {
808 color: #00b9fd;
809}
810
811#frontcanvas,
812#backcanvas {
813 touch-action: none;
814}
815
816.placeholder {
817 border: 1px dashed #9f9fda !important;
818 background-color: #edf2f7 !important;
819}
820
821.dragging {
822 z-index: 999;
823}
824
825.dark .dragging>table>tbody>tr {
826 background-color: #252c30;
827}
828
829.dark .placeholder {
830 filter: invert(1);
831}
832
833.column-spacer {
834 top: 0;
835 left: 0;
836 width: calc(100% - 4px);
837 position: absolute;
838 cursor: pointer;
839 user-select: none;
840 height: 100%;
841}
842
843.column-width-handle {
844 top: 0;
845 right: 0;
846 width: 4px;
847 position: absolute;
848 cursor: col-resize;
849 user-select: none;
850 height: 100%;
851}
852
853.column-width-handle:hover {
854 background-color: #4f99bd;
855}
856
857.help-link {
858 border: 1px solid #0278a4;
859 padding-inline: 0.3rem;
860 border-radius: 3px;
861 cursor: pointer;
862}
863
864.dark .help-link {
865 border: 1px solid #00b9fd;
866}
867
868.bom-color {
869 width: 20%;
870}
871
872.color-column input {
873 width: 1.6rem;
874 height: 1rem;
875 border: 1px solid black;
876 cursor: pointer;
877 padding: 0;
878}
879
880/* removes default styling from input color element */
881::-webkit-color-swatch {
882 border: none;
883}
884
885::-webkit-color-swatch-wrapper {
886 padding: 0;
887}
888
889::-moz-color-swatch,
890::-moz-focus-inner {
891 border: none;
892}
893
894::-moz-focus-inner {
895 padding: 0;
896}
897
898 </style>
899 <script type="text/javascript" >
900///////////////////////////////////////////////
901/*
902 Split.js - v1.3.5
903 MIT License
904 https://github.com/nathancahill/Split.js
905*/
906!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}}});
907
908///////////////////////////////////////////////
909
910///////////////////////////////////////////////
911// Copyright (c) 2013 Pieroxy <pieroxy@pieroxy.net>
912// This work is free. You can redistribute it and/or modify it
913// under the terms of the WTFPL, Version 2
914// For more information see LICENSE.txt or http://www.wtfpl.net/
915//
916// For more information, the home page:
917// http://pieroxy.net/blog/pages/lz-string/testing.html
918//
919// LZ-based compression algorithm, version 1.4.4
920var 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});
921///////////////////////////////////////////////
922
923///////////////////////////////////////////////
924/*!
925 * PEP v0.4.3 | https://github.com/jquery/PEP
926 * Copyright jQuery Foundation and other contributors | http://jquery.org/license
927 */
928!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);
929for(var d,e=2;e<m.length;e++)d=m[e],c[d]=b[d]||n[e];c.buttons=b.buttons||0;
930var 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}})}
931var 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++)
932b.register.call(b,a)},unregister:function(a){for(var b,c=this.eventSourceList.length,d=0;d<c&&(b=this.eventSourceList[d]);d++)
933b.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){
934for(var b=a;b.parentNode;)b=b.parentNode;
935return 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);
936return 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){
937//
938this.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))},
939installOnLoad: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){
940var b=x(a,this.findElements,this);
941return 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){}
942var 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:[],
943isEventSimulatedFromTouch: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++){
944var 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);
9450!==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];
946c.buttons=b?b.buttons&~d:0,a.buttons=c.buttons}H.set(this.POINTER_ID,a),
947c.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),
948Q(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),
949Q(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);
950d&&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){
951(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,
952c.altKey=b.altKey,c.ctrlKey=b.ctrlKey,c.metaKey=b.metaKey,c.shiftKey=b.shiftKey;
953var 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))},
954shouldScroll:function(a){if(this.firstXY){var b,c=a.currentTarget._scrollType;if("none"===c)
955b=!1;else if("XY"===c)
956b=!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]);
957b=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},
958vacuumTouches:function(a){var b=a.touches;
959if(R.size>=b.length){var c=[];R.forEach(function(a,d){
960if(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);
961if(c){var d=c.out,e=c.outTarget;u.move(b),d&&e!==b.target&&(d.relatedTarget=b.target,b.relatedTarget=e,
962d.target=e,b.target?(u.leaveOut(d),u.enterOver(b)):(
963b.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)},
964dedupSynthMouse:function(a){var b=N.lastTouches,c=a.changedTouches[0];
965if(this.isPrimaryTouch(c)){
966var 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});
967
968///////////////////////////////////////////////
969
970///////////////////////////////////////////////
971var config = {"dark_mode": false, "show_pads": true, "show_fabrication": false, "show_silkscreen": true, "highlight_pin1": "none", "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"]}
972///////////////////////////////////////////////
973
974///////////////////////////////////////////////
975var pcbdata = JSON.parse(LZString.decompressFromBase64("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"))
976///////////////////////////////////////////////
977
978///////////////////////////////////////////////
979/* Utility functions */
980
981var storagePrefix = 'KiCad_HTML_BOM__' + pcbdata.metadata.title + '__' +
982 pcbdata.metadata.revision + '__#';
983var storage;
984
985function initStorage(key) {
986 try {
987 window.localStorage.getItem("blank");
988 storage = window.localStorage;
989 } catch (e) {
990 // localStorage not available
991 }
992 if (!storage) {
993 try {
994 window.sessionStorage.getItem("blank");
995 storage = window.sessionStorage;
996 } catch (e) {
997 // sessionStorage also not available
998 }
999 }
1000}
1001
1002function readStorage(key) {
1003 if (storage) {
1004 return storage.getItem(storagePrefix + key);
1005 } else {
1006 return null;
1007 }
1008}
1009
1010function writeStorage(key, value) {
1011 if (storage) {
1012 storage.setItem(storagePrefix + key, value);
1013 }
1014}
1015
1016function fancyDblClickHandler(el, onsingle, ondouble) {
1017 return function () {
1018 if (el.getAttribute("data-dblclick") == null) {
1019 el.setAttribute("data-dblclick", 1);
1020 setTimeout(function () {
1021 if (el.getAttribute("data-dblclick") == 1) {
1022 onsingle();
1023 }
1024 el.removeAttribute("data-dblclick");
1025 }, 200);
1026 } else {
1027 el.removeAttribute("data-dblclick");
1028 ondouble();
1029 }
1030 }
1031}
1032
1033function smoothScrollToRow(rowid) {
1034 document.getElementById(rowid).scrollIntoView({
1035 behavior: "smooth",
1036 block: "center",
1037 inline: "nearest"
1038 });
1039}
1040
1041function focusInputField(input) {
1042 input.scrollIntoView(false);
1043 input.focus();
1044 input.select();
1045}
1046
1047function saveBomTable(output) {
1048 var text = '';
1049 for (var node of bomhead.childNodes[0].childNodes) {
1050 if (node.firstChild) {
1051 var name = node.firstChild.nodeValue ?? "";
1052 text += (output == 'csv' ? `"${name}"` : name);
1053 }
1054 if (node != bomhead.childNodes[0].lastChild) {
1055 text += (output == 'csv' ? ',' : '\t');
1056 }
1057 }
1058 text += '\n';
1059 for (var row of bombody.childNodes) {
1060 for (var cell of row.childNodes) {
1061 let val = '';
1062 for (var node of cell.childNodes) {
1063 if (node.nodeName == "INPUT") {
1064 if (node.checked) {
1065 val += '✓';
1066 }
1067 } else if ((node.nodeName == "MARK") || (node.nodeName == "A")) {
1068 val += node.firstChild.nodeValue;
1069 } else {
1070 val += node.nodeValue;
1071 }
1072 }
1073 if (output == 'csv') {
1074 val = val.replace(/\"/g, '\"\"'); // pair of double-quote characters
1075 if (isNumeric(val)) {
1076 val = +val; // use number
1077 } else {
1078 val = `"${val}"`; // enclosed within double-quote
1079 }
1080 }
1081 text += val;
1082 if (cell != row.lastChild) {
1083 text += (output == 'csv' ? ',' : '\t');
1084 }
1085 }
1086 text += '\n';
1087 }
1088
1089 if (output != 'clipboard') {
1090 // To file: csv or txt
1091 var blob = new Blob([text], {
1092 type: `text/${output}`
1093 });
1094 saveFile(`${pcbdata.metadata.title}.${output}`, blob);
1095 } else {
1096 // To clipboard
1097 var textArea = document.createElement("textarea");
1098 textArea.classList.add('clipboard-temp');
1099 textArea.value = text;
1100
1101 document.body.appendChild(textArea);
1102 textArea.focus();
1103 textArea.select();
1104
1105 try {
1106 if (document.execCommand('copy')) {
1107 console.log('Bom copied to clipboard.');
1108 }
1109 } catch (err) {
1110 console.log('Can not copy to clipboard.');
1111 }
1112
1113 document.body.removeChild(textArea);
1114 }
1115}
1116
1117function isNumeric(str) {
1118 /* https://stackoverflow.com/a/175787 */
1119 return (typeof str != "string" ? false : !isNaN(str) && !isNaN(parseFloat(str)));
1120}
1121
1122function removeGutterNode(node) {
1123 for (var i = 0; i < node.childNodes.length; i++) {
1124 if (node.childNodes[i].classList &&
1125 node.childNodes[i].classList.contains("gutter")) {
1126 node.removeChild(node.childNodes[i]);
1127 break;
1128 }
1129 }
1130}
1131
1132function cleanGutters() {
1133 removeGutterNode(document.getElementById("bot"));
1134 removeGutterNode(document.getElementById("canvasdiv"));
1135}
1136
1137var units = {
1138 prefixes: {
1139 giga: ["G", "g", "giga", "Giga", "GIGA"],
1140 mega: ["M", "mega", "Mega", "MEGA"],
1141 kilo: ["K", "k", "kilo", "Kilo", "KILO"],
1142 milli: ["m", "milli", "Milli", "MILLI"],
1143 micro: ["U", "u", "micro", "Micro", "MICRO", "μ", "µ"], // different utf8 μ
1144 nano: ["N", "n", "nano", "Nano", "NANO"],
1145 pico: ["P", "p", "pico", "Pico", "PICO"],
1146 },
1147 unitsShort: ["R", "r", "Ω", "F", "f", "H", "h"],
1148 unitsLong: [
1149 "OHM", "Ohm", "ohm", "ohms",
1150 "FARAD", "Farad", "farad",
1151 "HENRY", "Henry", "henry"
1152 ],
1153 getMultiplier: function (s) {
1154 if (this.prefixes.giga.includes(s)) return 1e9;
1155 if (this.prefixes.mega.includes(s)) return 1e6;
1156 if (this.prefixes.kilo.includes(s)) return 1e3;
1157 if (this.prefixes.milli.includes(s)) return 1e-3;
1158 if (this.prefixes.micro.includes(s)) return 1e-6;
1159 if (this.prefixes.nano.includes(s)) return 1e-9;
1160 if (this.prefixes.pico.includes(s)) return 1e-12;
1161 return 1;
1162 },
1163 valueRegex: null,
1164 valueAltRegex: null,
1165}
1166
1167function initUtils() {
1168 var allPrefixes = units.prefixes.giga
1169 .concat(units.prefixes.mega)
1170 .concat(units.prefixes.kilo)
1171 .concat(units.prefixes.milli)
1172 .concat(units.prefixes.micro)
1173 .concat(units.prefixes.nano)
1174 .concat(units.prefixes.pico);
1175 var allUnits = units.unitsShort.concat(units.unitsLong);
1176 units.valueRegex = new RegExp("^([0-9\.]+)" +
1177 "\\s*(" + allPrefixes.join("|") + ")?" +
1178 "(" + allUnits.join("|") + ")?" +
1179 "(\\b.*)?$", "");
1180 units.valueAltRegex = new RegExp("^([0-9]*)" +
1181 "(" + units.unitsShort.join("|") + ")?" +
1182 "([GgMmKkUuNnPp])?" +
1183 "([0-9]*)" +
1184 "(\\b.*)?$", "");
1185 if (config.fields.includes("Value")) {
1186 var index = config.fields.indexOf("Value");
1187 pcbdata.bom["parsedValues"] = {};
1188 var allList = getBomListByLayer('FB').flat();
1189 for (var id in pcbdata.bom.fields) {
1190 var ref_key = allList.find(item => item[1] == Number(id)) || [];
1191 pcbdata.bom.parsedValues[id] = parseValue(pcbdata.bom.fields[id][index], ref_key[0] || '');
1192 }
1193 }
1194}
1195
1196function parseValue(val, ref) {
1197 var inferUnit = (unit, ref) => {
1198 if (unit) {
1199 unit = unit.toLowerCase();
1200 if (unit == 'Ω' || unit == "ohm" || unit == "ohms") {
1201 unit = 'r';
1202 }
1203 return unit[0];
1204 }
1205
1206 var resarr = /^([a-z]+)\d+$/i.exec(ref);
1207 switch (Array.isArray(resarr) && resarr[1].toLowerCase()) {
1208 case "c": return 'f';
1209 case "l": return 'h';
1210 case "r":
1211 case "rv": return 'r';
1212 }
1213 return null;
1214 };
1215 val = val.replace(/,/g, "");
1216 var match = units.valueRegex.exec(val);
1217 if (Array.isArray(match)) {
1218 var unit = inferUnit(match[3], ref);
1219 var val_i = parseFloat(match[1]);
1220 if (!unit) return null;
1221 if (match[2]) {
1222 val_i = val_i * units.getMultiplier(match[2]);
1223 }
1224 return {
1225 val: val_i,
1226 unit: unit,
1227 extra: match[4],
1228 }
1229 }
1230
1231 match = units.valueAltRegex.exec(val);
1232 if (Array.isArray(match) && (match[1] || match[4])) {
1233 var unit = inferUnit(match[2], ref);
1234 var val_i = parseFloat(match[1] + "." + match[4]);
1235 if (!unit) return null;
1236 if (match[3]) {
1237 val_i = val_i * units.getMultiplier(match[3]);
1238 }
1239 return {
1240 val: val_i,
1241 unit: unit,
1242 extra: match[5],
1243 }
1244 }
1245 return null;
1246}
1247
1248function valueCompare(a, b, stra, strb) {
1249 if (a === null && b === null) {
1250 // Failed to parse both values, compare them as strings.
1251 if (stra != strb) return stra > strb ? 1 : -1;
1252 else return 0;
1253 } else if (a === null) {
1254 return 1;
1255 } else if (b === null) {
1256 return -1;
1257 } else {
1258 if (a.unit != b.unit) return a.unit > b.unit ? 1 : -1;
1259 else if (a.val != b.val) return a.val > b.val ? 1 : -1;
1260 else if (a.extra != b.extra) return a.extra > b.extra ? 1 : -1;
1261 else return 0;
1262 }
1263}
1264
1265function validateSaveImgDimension(element) {
1266 var valid = false;
1267 var intValue = 0;
1268 if (/^[1-9]\d*$/.test(element.value)) {
1269 intValue = parseInt(element.value);
1270 if (intValue <= 16000) {
1271 valid = true;
1272 }
1273 }
1274 if (valid) {
1275 element.classList.remove("invalid");
1276 } else {
1277 element.classList.add("invalid");
1278 }
1279 return intValue;
1280}
1281
1282function saveImage(layer) {
1283 var width = validateSaveImgDimension(document.getElementById("render-save-width"));
1284 var height = validateSaveImgDimension(document.getElementById("render-save-height"));
1285 var bgcolor = null;
1286 if (!document.getElementById("render-save-transparent").checked) {
1287 var style = getComputedStyle(topmostdiv);
1288 bgcolor = style.getPropertyValue("background-color");
1289 }
1290 if (!width || !height) return;
1291
1292 // Prepare image
1293 var canvas = document.createElement("canvas");
1294 var layerdict = {
1295 transform: {
1296 x: 0,
1297 y: 0,
1298 s: 1,
1299 panx: 0,
1300 pany: 0,
1301 zoom: 1,
1302 },
1303 bg: canvas,
1304 fab: canvas,
1305 silk: canvas,
1306 highlight: canvas,
1307 layer: layer,
1308 }
1309 // Do the rendering
1310 recalcLayerScale(layerdict, width, height);
1311 prepareLayer(layerdict);
1312 clearCanvas(canvas, bgcolor);
1313 drawBackground(layerdict, false);
1314 drawHighlightsOnLayer(layerdict, false);
1315
1316 // Save image
1317 var imgdata = canvas.toDataURL("image/png");
1318
1319 var filename = pcbdata.metadata.title;
1320 if (pcbdata.metadata.revision) {
1321 filename += `.${pcbdata.metadata.revision}`;
1322 }
1323 filename += `.${layer}.png`;
1324 saveFile(filename, dataURLtoBlob(imgdata));
1325}
1326
1327function saveSettings() {
1328 var data = {
1329 type: "InteractiveHtmlBom settings",
1330 version: 1,
1331 pcbmetadata: pcbdata.metadata,
1332 settings: settings,
1333 }
1334 var blob = new Blob([JSON.stringify(data, null, 4)], {
1335 type: "application/json"
1336 });
1337 saveFile(`${pcbdata.metadata.title}.settings.json`, blob);
1338}
1339
1340function loadSettings() {
1341 var input = document.createElement("input");
1342 input.type = "file";
1343 input.accept = ".settings.json";
1344 input.onchange = function (e) {
1345 var file = e.target.files[0];
1346 var reader = new FileReader();
1347 reader.onload = readerEvent => {
1348 var content = readerEvent.target.result;
1349 var newSettings;
1350 try {
1351 newSettings = JSON.parse(content);
1352 } catch (e) {
1353 alert("Selected file is not InteractiveHtmlBom settings file.");
1354 return;
1355 }
1356 if (newSettings.type != "InteractiveHtmlBom settings") {
1357 alert("Selected file is not InteractiveHtmlBom settings file.");
1358 return;
1359 }
1360 var metadataMatches = newSettings.hasOwnProperty("pcbmetadata");
1361 if (metadataMatches) {
1362 for (var k in pcbdata.metadata) {
1363 if (!newSettings.pcbmetadata.hasOwnProperty(k) || newSettings.pcbmetadata[k] != pcbdata.metadata[k]) {
1364 metadataMatches = false;
1365 }
1366 }
1367 }
1368 if (!metadataMatches) {
1369 var currentMetadata = JSON.stringify(pcbdata.metadata, null, 4);
1370 var fileMetadata = JSON.stringify(newSettings.pcbmetadata, null, 4);
1371 if (!confirm(
1372 `Settins file metadata does not match current metadata.\n\n` +
1373 `Page metadata:\n${currentMetadata}\n\n` +
1374 `Settings file metadata:\n${fileMetadata}\n\n` +
1375 `Press OK if you would like to import settings anyway.`)) {
1376 return;
1377 }
1378 }
1379 overwriteSettings(newSettings.settings);
1380 }
1381 reader.readAsText(file, 'UTF-8');
1382 }
1383 input.click();
1384}
1385
1386function resetSettings() {
1387 if (!confirm(
1388 `This will reset all checkbox states and other settings.\n\n` +
1389 `Press OK if you want to continue.`)) {
1390 return;
1391 }
1392 if (storage) {
1393 var keys = [];
1394 for (var i = 0; i < storage.length; i++) {
1395 var key = storage.key(i);
1396 if (key.startsWith(storagePrefix)) keys.push(key);
1397 }
1398 for (var key of keys) storage.removeItem(key);
1399 }
1400 location.reload();
1401}
1402
1403function overwriteSettings(newSettings) {
1404 initDone = false;
1405 Object.assign(settings, newSettings);
1406 writeStorage("bomlayout", settings.bomlayout);
1407 writeStorage("bommode", settings.bommode);
1408 writeStorage("canvaslayout", settings.canvaslayout);
1409 writeStorage("bomCheckboxes", settings.checkboxes.join(","));
1410 document.getElementById("bomCheckboxes").value = settings.checkboxes.join(",");
1411 for (var checkbox of settings.checkboxes) {
1412 writeStorage("checkbox_" + checkbox, settings.checkboxStoredRefs[checkbox]);
1413 }
1414 writeStorage("markWhenChecked", settings.markWhenChecked);
1415 padsVisible(settings.renderPads);
1416 document.getElementById("padsCheckbox").checked = settings.renderPads;
1417 fabricationVisible(settings.renderFabrication);
1418 document.getElementById("fabricationCheckbox").checked = settings.renderFabrication;
1419 silkscreenVisible(settings.renderSilkscreen);
1420 document.getElementById("silkscreenCheckbox").checked = settings.renderSilkscreen;
1421 referencesVisible(settings.renderReferences);
1422 document.getElementById("referencesCheckbox").checked = settings.renderReferences;
1423 valuesVisible(settings.renderValues);
1424 document.getElementById("valuesCheckbox").checked = settings.renderValues;
1425 tracksVisible(settings.renderTracks);
1426 document.getElementById("tracksCheckbox").checked = settings.renderTracks;
1427 zonesVisible(settings.renderZones);
1428 document.getElementById("zonesCheckbox").checked = settings.renderZones;
1429 dnpOutline(settings.renderDnpOutline);
1430 document.getElementById("dnpOutlineCheckbox").checked = settings.renderDnpOutline;
1431 setRedrawOnDrag(settings.redrawOnDrag);
1432 document.getElementById("dragCheckbox").checked = settings.redrawOnDrag;
1433 setHighlightRowOnClick(settings.highlightRowOnClick);
1434 document.getElementById("highlightRowOnClickCheckbox").checked = settings.highlightRowOnClick;
1435 setDarkMode(settings.darkMode);
1436 document.getElementById("darkmodeCheckbox").checked = settings.darkMode;
1437 setHighlightPin1(settings.highlightpin1);
1438 document.forms.highlightpin1.highlightpin1.value = settings.highlightpin1;
1439 writeStorage("boardRotation", settings.boardRotation);
1440 document.getElementById("boardRotation").value = settings.boardRotation / 5;
1441 document.getElementById("rotationDegree").textContent = settings.boardRotation;
1442 setOffsetBackRotation(settings.offsetBackRotation);
1443 document.getElementById("offsetBackRotationCheckbox").checked = settings.offsetBackRotation;
1444 initDone = true;
1445 prepCheckboxes();
1446 changeBomLayout(settings.bomlayout);
1447}
1448
1449function saveFile(filename, blob) {
1450 var link = document.createElement("a");
1451 var objurl = URL.createObjectURL(blob);
1452 link.download = filename;
1453 link.href = objurl;
1454 link.click();
1455}
1456
1457function dataURLtoBlob(dataurl) {
1458 var arr = dataurl.split(','),
1459 mime = arr[0].match(/:(.*?);/)[1],
1460 bstr = atob(arr[1]),
1461 n = bstr.length,
1462 u8arr = new Uint8Array(n);
1463 while (n--) {
1464 u8arr[n] = bstr.charCodeAt(n);
1465 }
1466 return new Blob([u8arr], {
1467 type: mime
1468 });
1469}
1470
1471var settings = {
1472 canvaslayout: "FB",
1473 bomlayout: "left-right",
1474 bommode: "grouped",
1475 checkboxes: [],
1476 checkboxStoredRefs: {},
1477 darkMode: false,
1478 highlightpin1: "none",
1479 redrawOnDrag: true,
1480 boardRotation: 0,
1481 offsetBackRotation: false,
1482 renderPads: true,
1483 renderReferences: true,
1484 renderValues: true,
1485 renderSilkscreen: true,
1486 renderFabrication: true,
1487 renderDnpOutline: false,
1488 renderTracks: true,
1489 renderZones: true,
1490 columnOrder: [],
1491 hiddenColumns: [],
1492 netColors: {},
1493}
1494
1495function initDefaults() {
1496 settings.bomlayout = readStorage("bomlayout");
1497 if (settings.bomlayout === null) {
1498 settings.bomlayout = config.bom_view;
1499 }
1500 if (!['bom-only', 'left-right', 'top-bottom'].includes(settings.bomlayout)) {
1501 settings.bomlayout = config.bom_view;
1502 }
1503 settings.bommode = readStorage("bommode");
1504 if (settings.bommode === null) {
1505 settings.bommode = "grouped";
1506 }
1507 if (settings.bommode == "netlist" && !pcbdata.nets) {
1508 settings.bommode = "grouped";
1509 }
1510 if (!["grouped", "ungrouped", "netlist"].includes(settings.bommode)) {
1511 settings.bommode = "grouped";
1512 }
1513 settings.canvaslayout = readStorage("canvaslayout");
1514 if (settings.canvaslayout === null) {
1515 settings.canvaslayout = config.layer_view;
1516 }
1517 var bomCheckboxes = readStorage("bomCheckboxes");
1518 if (bomCheckboxes === null) {
1519 bomCheckboxes = config.checkboxes;
1520 }
1521 settings.checkboxes = bomCheckboxes.split(",").filter((e) => e);
1522 document.getElementById("bomCheckboxes").value = bomCheckboxes;
1523
1524 var highlightpin1 = readStorage("highlightpin1") || config.highlight_pin1;
1525 if (highlightpin1 === "false") highlightpin1 = "none";
1526 if (highlightpin1 === "true") highlightpin1 = "all";
1527 setHighlightPin1(highlightpin1);
1528 document.forms.highlightpin1.highlightpin1.value = highlightpin1;
1529
1530 settings.markWhenChecked = readStorage("markWhenChecked") || "";
1531 populateMarkWhenCheckedOptions();
1532
1533 function initBooleanSetting(storageString, def, elementId, func) {
1534 var b = readStorage(storageString);
1535 if (b === null) {
1536 b = def;
1537 } else {
1538 b = (b == "true");
1539 }
1540 document.getElementById(elementId).checked = b;
1541 func(b);
1542 }
1543
1544 initBooleanSetting("padsVisible", config.show_pads, "padsCheckbox", padsVisible);
1545 initBooleanSetting("fabricationVisible", config.show_fabrication, "fabricationCheckbox", fabricationVisible);
1546 initBooleanSetting("silkscreenVisible", config.show_silkscreen, "silkscreenCheckbox", silkscreenVisible);
1547 initBooleanSetting("referencesVisible", true, "referencesCheckbox", referencesVisible);
1548 initBooleanSetting("valuesVisible", true, "valuesCheckbox", valuesVisible);
1549 if ("tracks" in pcbdata) {
1550 initBooleanSetting("tracksVisible", true, "tracksCheckbox", tracksVisible);
1551 initBooleanSetting("zonesVisible", true, "zonesCheckbox", zonesVisible);
1552 } else {
1553 document.getElementById("tracksAndZonesCheckboxes").style.display = "none";
1554 tracksVisible(false);
1555 zonesVisible(false);
1556 }
1557 initBooleanSetting("dnpOutline", false, "dnpOutlineCheckbox", dnpOutline);
1558 initBooleanSetting("redrawOnDrag", config.redraw_on_drag, "dragCheckbox", setRedrawOnDrag);
1559 initBooleanSetting("highlightRowOnClick", false, "highlightRowOnClickCheckbox", setHighlightRowOnClick);
1560 initBooleanSetting("darkmode", config.dark_mode, "darkmodeCheckbox", setDarkMode);
1561
1562 var fields = ["checkboxes", "References"].concat(config.fields).concat(["Quantity"]);
1563 var hcols = JSON.parse(readStorage("hiddenColumns"));
1564 if (hcols === null) {
1565 hcols = [];
1566 }
1567 settings.hiddenColumns = hcols.filter(e => fields.includes(e));
1568
1569 var cord = JSON.parse(readStorage("columnOrder"));
1570 if (cord === null) {
1571 cord = fields;
1572 } else {
1573 cord = cord.filter(e => fields.includes(e));
1574 if (cord.length != fields.length)
1575 cord = fields;
1576 }
1577 settings.columnOrder = cord;
1578
1579 settings.boardRotation = readStorage("boardRotation");
1580 if (settings.boardRotation === null) {
1581 settings.boardRotation = config.board_rotation * 5;
1582 } else {
1583 settings.boardRotation = parseInt(settings.boardRotation);
1584 }
1585 document.getElementById("boardRotation").value = settings.boardRotation / 5;
1586 document.getElementById("rotationDegree").textContent = settings.boardRotation;
1587 initBooleanSetting("offsetBackRotation", config.offset_back_rotation, "offsetBackRotationCheckbox", setOffsetBackRotation);
1588
1589 settings.netColors = JSON.parse(readStorage("netColors")) || {};
1590}
1591
1592// Helper classes for user js callbacks.
1593
1594const IBOM_EVENT_TYPES = {
1595 ALL: "all",
1596 HIGHLIGHT_EVENT: "highlightEvent",
1597 CHECKBOX_CHANGE_EVENT: "checkboxChangeEvent",
1598 BOM_BODY_CHANGE_EVENT: "bomBodyChangeEvent",
1599}
1600
1601const EventHandler = {
1602 callbacks: {},
1603 init: function () {
1604 for (eventType of Object.values(IBOM_EVENT_TYPES))
1605 this.callbacks[eventType] = [];
1606 },
1607 registerCallback: function (eventType, callback) {
1608 this.callbacks[eventType].push(callback);
1609 },
1610 emitEvent: function (eventType, eventArgs) {
1611 event = {
1612 eventType: eventType,
1613 args: eventArgs,
1614 }
1615 var callback;
1616 for (callback of this.callbacks[eventType])
1617 callback(event);
1618 for (callback of this.callbacks[IBOM_EVENT_TYPES.ALL])
1619 callback(event);
1620 }
1621}
1622EventHandler.init();
1623
1624///////////////////////////////////////////////
1625
1626///////////////////////////////////////////////
1627/* PCB rendering code */
1628
1629var emptyContext2d = document.createElement("canvas").getContext("2d");
1630
1631function deg2rad(deg) {
1632 return deg * Math.PI / 180;
1633}
1634
1635function calcFontPoint(linepoint, text, offsetx, offsety, tilt) {
1636 var point = [
1637 linepoint[0] * text.width + offsetx,
1638 linepoint[1] * text.height + offsety
1639 ];
1640 // This approximates pcbnew behavior with how text tilts depending on horizontal justification
1641 point[0] -= (linepoint[1] + 0.5 * (1 + text.justify[0])) * text.height * tilt;
1642 return point;
1643}
1644
1645function drawText(ctx, text, color) {
1646 if ("ref" in text && !settings.renderReferences) return;
1647 if ("val" in text && !settings.renderValues) return;
1648 ctx.save();
1649 ctx.fillStyle = color;
1650 ctx.strokeStyle = color;
1651 ctx.lineCap = "round";
1652 ctx.lineJoin = "round";
1653 ctx.lineWidth = text.thickness;
1654 if ("svgpath" in text) {
1655 ctx.stroke(new Path2D(text.svgpath));
1656 ctx.restore();
1657 return;
1658 }
1659 if ("polygons" in text) {
1660 ctx.fill(getPolygonsPath(text));
1661 ctx.restore();
1662 return;
1663 }
1664 ctx.translate(...text.pos);
1665 ctx.translate(text.thickness * 0.5, 0);
1666 var angle = -text.angle;
1667 if (text.attr.includes("mirrored")) {
1668 ctx.scale(-1, 1);
1669 angle = -angle;
1670 }
1671 var tilt = 0;
1672 if (text.attr.includes("italic")) {
1673 tilt = 0.125;
1674 }
1675 var interline = text.height * 1.5 + text.thickness;
1676 var txt = text.text.split("\n");
1677 // KiCad ignores last empty line.
1678 if (txt[txt.length - 1] == '') txt.pop();
1679 ctx.rotate(deg2rad(angle));
1680 var offsety = (1 - text.justify[1]) / 2 * text.height; // One line offset
1681 offsety -= (txt.length - 1) * (text.justify[1] + 1) / 2 * interline; // Multiline offset
1682 for (var i in txt) {
1683 var lineWidth = text.thickness + interline / 2 * tilt;
1684 for (var j = 0; j < txt[i].length; j++) {
1685 if (txt[i][j] == '\t') {
1686 var fourSpaces = 4 * pcbdata.font_data[' '].w * text.width;
1687 lineWidth += fourSpaces - lineWidth % fourSpaces;
1688 } else {
1689 if (txt[i][j] == '~') {
1690 j++;
1691 if (j == txt[i].length)
1692 break;
1693 }
1694 lineWidth += pcbdata.font_data[txt[i][j]].w * text.width;
1695 }
1696 }
1697 var offsetx = -lineWidth * (text.justify[0] + 1) / 2;
1698 var inOverbar = false;
1699 for (var j = 0; j < txt[i].length; j++) {
1700 if (config.kicad_text_formatting) {
1701 if (txt[i][j] == '\t') {
1702 var fourSpaces = 4 * pcbdata.font_data[' '].w * text.width;
1703 offsetx += fourSpaces - offsetx % fourSpaces;
1704 continue;
1705 } else if (txt[i][j] == '~') {
1706 j++;
1707 if (j == txt[i].length)
1708 break;
1709 if (txt[i][j] != '~') {
1710 inOverbar = !inOverbar;
1711 }
1712 }
1713 }
1714 var glyph = pcbdata.font_data[txt[i][j]];
1715 if (inOverbar) {
1716 var overbarStart = [offsetx, -text.height * 1.4 + offsety];
1717 var overbarEnd = [offsetx + text.width * glyph.w, overbarStart[1]];
1718
1719 if (!lastHadOverbar) {
1720 overbarStart[0] += text.height * 1.4 * tilt;
1721 lastHadOverbar = true;
1722 }
1723 ctx.beginPath();
1724 ctx.moveTo(...overbarStart);
1725 ctx.lineTo(...overbarEnd);
1726 ctx.stroke();
1727 } else {
1728 lastHadOverbar = false;
1729 }
1730 for (var line of glyph.l) {
1731 ctx.beginPath();
1732 ctx.moveTo(...calcFontPoint(line[0], text, offsetx, offsety, tilt));
1733 for (var k = 1; k < line.length; k++) {
1734 ctx.lineTo(...calcFontPoint(line[k], text, offsetx, offsety, tilt));
1735 }
1736 ctx.stroke();
1737 }
1738 offsetx += glyph.w * text.width;
1739 }
1740 offsety += interline;
1741 }
1742 ctx.restore();
1743}
1744
1745function drawedge(ctx, scalefactor, edge, color) {
1746 ctx.strokeStyle = color;
1747 ctx.fillStyle = color;
1748 ctx.lineWidth = Math.max(1 / scalefactor, edge.width);
1749 ctx.lineCap = "round";
1750 ctx.lineJoin = "round";
1751 if ("svgpath" in edge) {
1752 ctx.stroke(new Path2D(edge.svgpath));
1753 } else {
1754 ctx.beginPath();
1755 if (edge.type == "segment") {
1756 ctx.moveTo(...edge.start);
1757 ctx.lineTo(...edge.end);
1758 }
1759 if (edge.type == "rect") {
1760 ctx.moveTo(...edge.start);
1761 ctx.lineTo(edge.start[0], edge.end[1]);
1762 ctx.lineTo(...edge.end);
1763 ctx.lineTo(edge.end[0], edge.start[1]);
1764 ctx.lineTo(...edge.start);
1765 }
1766 if (edge.type == "arc") {
1767 ctx.arc(
1768 ...edge.start,
1769 edge.radius,
1770 deg2rad(edge.startangle),
1771 deg2rad(edge.endangle));
1772 }
1773 if (edge.type == "circle") {
1774 ctx.arc(
1775 ...edge.start,
1776 edge.radius,
1777 0, 2 * Math.PI);
1778 ctx.closePath();
1779 }
1780 if (edge.type == "curve") {
1781 ctx.moveTo(...edge.start);
1782 ctx.bezierCurveTo(...edge.cpa, ...edge.cpb, ...edge.end);
1783 }
1784 if("filled" in edge && edge.filled)
1785 ctx.fill();
1786 else
1787 ctx.stroke();
1788 }
1789}
1790
1791function getChamferedRectPath(size, radius, chamfpos, chamfratio) {
1792 // chamfpos is a bitmask, left = 1, right = 2, bottom left = 4, bottom right = 8
1793 var path = new Path2D();
1794 var width = size[0];
1795 var height = size[1];
1796 var x = width * -0.5;
1797 var y = height * -0.5;
1798 var chamfOffset = Math.min(width, height) * chamfratio;
1799 path.moveTo(x, 0);
1800 if (chamfpos & 4) {
1801 path.lineTo(x, y + height - chamfOffset);
1802 path.lineTo(x + chamfOffset, y + height);
1803 path.lineTo(0, y + height);
1804 } else {
1805 path.arcTo(x, y + height, x + width, y + height, radius);
1806 }
1807 if (chamfpos & 8) {
1808 path.lineTo(x + width - chamfOffset, y + height);
1809 path.lineTo(x + width, y + height - chamfOffset);
1810 path.lineTo(x + width, 0);
1811 } else {
1812 path.arcTo(x + width, y + height, x + width, y, radius);
1813 }
1814 if (chamfpos & 2) {
1815 path.lineTo(x + width, y + chamfOffset);
1816 path.lineTo(x + width - chamfOffset, y);
1817 path.lineTo(0, y);
1818 } else {
1819 path.arcTo(x + width, y, x, y, radius);
1820 }
1821 if (chamfpos & 1) {
1822 path.lineTo(x + chamfOffset, y);
1823 path.lineTo(x, y + chamfOffset);
1824 path.lineTo(x, 0);
1825 } else {
1826 path.arcTo(x, y, x, y + height, radius);
1827 }
1828 path.closePath();
1829 return path;
1830}
1831
1832function getOblongPath(size) {
1833 return getChamferedRectPath(size, Math.min(size[0], size[1]) / 2, 0, 0);
1834}
1835
1836function getPolygonsPath(shape) {
1837 if (shape.path2d) {
1838 return shape.path2d;
1839 }
1840 if ("svgpath" in shape) {
1841 shape.path2d = new Path2D(shape.svgpath);
1842 } else {
1843 var path = new Path2D();
1844 for (var polygon of shape.polygons) {
1845 path.moveTo(...polygon[0]);
1846 for (var i = 1; i < polygon.length; i++) {
1847 path.lineTo(...polygon[i]);
1848 }
1849 path.closePath();
1850 }
1851 shape.path2d = path;
1852 }
1853 return shape.path2d;
1854}
1855
1856function drawPolygonShape(ctx, scalefactor, shape, color) {
1857 ctx.save();
1858 if (!("svgpath" in shape)) {
1859 ctx.translate(...shape.pos);
1860 ctx.rotate(deg2rad(-shape.angle));
1861 }
1862 if("filled" in shape && !shape.filled) {
1863 ctx.strokeStyle = color;
1864 ctx.lineWidth = Math.max(1 / scalefactor, shape.width);
1865 ctx.lineCap = "round";
1866 ctx.lineJoin = "round";
1867 ctx.stroke(getPolygonsPath(shape));
1868 } else {
1869 ctx.fillStyle = color;
1870 ctx.fill(getPolygonsPath(shape));
1871 }
1872 ctx.restore();
1873}
1874
1875function drawDrawing(ctx, scalefactor, drawing, color) {
1876 if (["segment", "arc", "circle", "curve", "rect"].includes(drawing.type)) {
1877 drawedge(ctx, scalefactor, drawing, color);
1878 } else if (drawing.type == "polygon") {
1879 drawPolygonShape(ctx, scalefactor, drawing, color);
1880 } else {
1881 drawText(ctx, drawing, color);
1882 }
1883}
1884
1885function getCirclePath(radius) {
1886 var path = new Path2D();
1887 path.arc(0, 0, radius, 0, 2 * Math.PI);
1888 path.closePath();
1889 return path;
1890}
1891
1892function getCachedPadPath(pad) {
1893 if (!pad.path2d) {
1894 // if path2d is not set, build one and cache it on pad object
1895 if (pad.shape == "rect") {
1896 pad.path2d = new Path2D();
1897 pad.path2d.rect(...pad.size.map(c => -c * 0.5), ...pad.size);
1898 } else if (pad.shape == "oval") {
1899 pad.path2d = getOblongPath(pad.size);
1900 } else if (pad.shape == "circle") {
1901 pad.path2d = getCirclePath(pad.size[0] / 2);
1902 } else if (pad.shape == "roundrect") {
1903 pad.path2d = getChamferedRectPath(pad.size, pad.radius, 0, 0);
1904 } else if (pad.shape == "chamfrect") {
1905 pad.path2d = getChamferedRectPath(pad.size, pad.radius, pad.chamfpos, pad.chamfratio)
1906 } else if (pad.shape == "custom") {
1907 pad.path2d = getPolygonsPath(pad);
1908 }
1909 }
1910 return pad.path2d;
1911}
1912
1913function drawPad(ctx, pad, color, outline) {
1914 ctx.save();
1915 ctx.translate(...pad.pos);
1916 ctx.rotate(-deg2rad(pad.angle));
1917 if (pad.offset) {
1918 ctx.translate(...pad.offset);
1919 }
1920 ctx.fillStyle = color;
1921 ctx.strokeStyle = color;
1922 var path = getCachedPadPath(pad);
1923 if (outline) {
1924 ctx.stroke(path);
1925 } else {
1926 ctx.fill(path);
1927 }
1928 ctx.restore();
1929}
1930
1931function drawPadHole(ctx, pad, padHoleColor) {
1932 if (pad.type != "th") return;
1933 ctx.save();
1934 ctx.translate(...pad.pos);
1935 ctx.rotate(-deg2rad(pad.angle));
1936 ctx.fillStyle = padHoleColor;
1937 if (pad.drillshape == "oblong") {
1938 ctx.fill(getOblongPath(pad.drillsize));
1939 } else if (pad.drillshape == "rect") {
1940 ctx.fill(getChamferedRectPath(pad.drillsize, 0, 0, 0));
1941 } else {
1942 ctx.fill(getCirclePath(pad.drillsize[0] / 2));
1943 }
1944 ctx.restore();
1945}
1946
1947function drawFootprint(ctx, layer, scalefactor, footprint, colors, highlight, outline) {
1948 if (highlight) {
1949 // draw bounding box
1950 if (footprint.layer == layer) {
1951 ctx.save();
1952 ctx.globalAlpha = 0.2;
1953 ctx.translate(...footprint.bbox.pos);
1954 ctx.rotate(deg2rad(-footprint.bbox.angle));
1955 ctx.translate(...footprint.bbox.relpos);
1956 ctx.fillStyle = colors.pad;
1957 ctx.fillRect(0, 0, ...footprint.bbox.size);
1958 ctx.globalAlpha = 1;
1959 ctx.strokeStyle = colors.pad;
1960 ctx.lineWidth = 3 / scalefactor;
1961 ctx.strokeRect(0, 0, ...footprint.bbox.size);
1962 ctx.restore();
1963 }
1964 }
1965 // draw drawings
1966 for (var drawing of footprint.drawings) {
1967 if (drawing.layer == layer) {
1968 drawDrawing(ctx, scalefactor, drawing.drawing, colors.pad);
1969 }
1970 }
1971 ctx.lineWidth = 3 / scalefactor;
1972 // draw pads
1973 if (settings.renderPads) {
1974 for (var pad of footprint.pads) {
1975 if (pad.layers.includes(layer)) {
1976 drawPad(ctx, pad, colors.pad, outline);
1977 if (pad.pin1 &&
1978 (settings.highlightpin1 == "all" ||
1979 settings.highlightpin1 == "selected" && highlight)) {
1980 drawPad(ctx, pad, colors.outline, true);
1981 }
1982 }
1983 }
1984 for (var pad of footprint.pads) {
1985 drawPadHole(ctx, pad, colors.padHole);
1986 }
1987 }
1988}
1989
1990function drawEdgeCuts(canvas, scalefactor) {
1991 var ctx = canvas.getContext("2d");
1992 var edgecolor = getComputedStyle(topmostdiv).getPropertyValue('--pcb-edge-color');
1993 for (var edge of pcbdata.edges) {
1994 drawDrawing(ctx, scalefactor, edge, edgecolor);
1995 }
1996}
1997
1998function drawFootprints(canvas, layer, scalefactor, highlight) {
1999 var ctx = canvas.getContext("2d");
2000 ctx.lineWidth = 3 / scalefactor;
2001 var style = getComputedStyle(topmostdiv);
2002
2003 var colors = {
2004 pad: style.getPropertyValue('--pad-color'),
2005 padHole: style.getPropertyValue('--pad-hole-color'),
2006 outline: style.getPropertyValue('--pin1-outline-color'),
2007 }
2008
2009 for (var i = 0; i < pcbdata.footprints.length; i++) {
2010 var mod = pcbdata.footprints[i];
2011 var outline = settings.renderDnpOutline && pcbdata.bom.skipped.includes(i);
2012 var h = highlightedFootprints.includes(i);
2013 var d = markedFootprints.has(i);
2014 if (highlight) {
2015 if(h && d) {
2016 colors.pad = style.getPropertyValue('--pad-color-highlight-both');
2017 colors.outline = style.getPropertyValue('--pin1-outline-color-highlight-both');
2018 } else if (h) {
2019 colors.pad = style.getPropertyValue('--pad-color-highlight');
2020 colors.outline = style.getPropertyValue('--pin1-outline-color-highlight');
2021 } else if (d) {
2022 colors.pad = style.getPropertyValue('--pad-color-highlight-marked');
2023 colors.outline = style.getPropertyValue('--pin1-outline-color-highlight-marked');
2024 }
2025 }
2026 if( h || d || !highlight) {
2027 drawFootprint(ctx, layer, scalefactor, mod, colors, highlight, outline);
2028 }
2029 }
2030}
2031
2032function drawBgLayer(layername, canvas, layer, scalefactor, edgeColor, polygonColor, textColor) {
2033 var ctx = canvas.getContext("2d");
2034 for (var d of pcbdata.drawings[layername][layer]) {
2035 if (["segment", "arc", "circle", "curve", "rect"].includes(d.type)) {
2036 drawedge(ctx, scalefactor, d, edgeColor);
2037 } else if (d.type == "polygon") {
2038 drawPolygonShape(ctx, scalefactor, d, polygonColor);
2039 } else {
2040 drawText(ctx, d, textColor);
2041 }
2042 }
2043}
2044
2045function drawTracks(canvas, layer, defaultColor, highlight) {
2046 ctx = canvas.getContext("2d");
2047 ctx.lineCap = "round";
2048
2049 var hasHole = (track) => (
2050 'drillsize' in track &&
2051 track.start[0] == track.end[0] &&
2052 track.start[1] == track.end[1]);
2053
2054 // First draw tracks and tented vias
2055 for (var track of pcbdata.tracks[layer]) {
2056 if (highlight && highlightedNet != track.net) continue;
2057 if (!hasHole(track)) {
2058 ctx.strokeStyle = highlight ? defaultColor : settings.netColors[track.net] || defaultColor;
2059 ctx.lineWidth = track.width;
2060 ctx.beginPath();
2061 if ('radius' in track) {
2062 ctx.arc(
2063 ...track.center,
2064 track.radius,
2065 deg2rad(track.startangle),
2066 deg2rad(track.endangle));
2067 } else {
2068 ctx.moveTo(...track.start);
2069 ctx.lineTo(...track.end);
2070 }
2071 ctx.stroke();
2072 }
2073 }
2074 // Second pass to draw untented vias
2075 var style = getComputedStyle(topmostdiv);
2076 var holeColor = style.getPropertyValue('--pad-hole-color')
2077
2078 for (var track of pcbdata.tracks[layer]) {
2079 if (highlight && highlightedNet != track.net) continue;
2080 if (hasHole(track)) {
2081 ctx.strokeStyle = highlight ? defaultColor : settings.netColors[track.net] || defaultColor;
2082 ctx.lineWidth = track.width;
2083 ctx.beginPath();
2084 ctx.moveTo(...track.start);
2085 ctx.lineTo(...track.end);
2086 ctx.stroke();
2087 ctx.strokeStyle = holeColor;
2088 ctx.lineWidth = track.drillsize;
2089 ctx.lineTo(...track.end);
2090 ctx.stroke();
2091 }
2092 }
2093}
2094
2095function drawZones(canvas, layer, defaultColor, highlight) {
2096 ctx = canvas.getContext("2d");
2097 ctx.lineJoin = "round";
2098 for (var zone of pcbdata.zones[layer]) {
2099 if (highlight && highlightedNet != zone.net) continue;
2100 ctx.strokeStyle = highlight ? defaultColor : settings.netColors[zone.net] || defaultColor;
2101 ctx.fillStyle = highlight ? defaultColor : settings.netColors[zone.net] || defaultColor;
2102 if (!zone.path2d) {
2103 zone.path2d = getPolygonsPath(zone);
2104 }
2105 ctx.fill(zone.path2d, zone.fillrule || "nonzero");
2106 if (zone.width > 0) {
2107 ctx.lineWidth = zone.width;
2108 ctx.stroke(zone.path2d);
2109 }
2110 }
2111}
2112
2113function clearCanvas(canvas, color = null) {
2114 var ctx = canvas.getContext("2d");
2115 ctx.save();
2116 ctx.setTransform(1, 0, 0, 1, 0, 0);
2117 if (color) {
2118 ctx.fillStyle = color;
2119 ctx.fillRect(0, 0, canvas.width, canvas.height);
2120 } else {
2121 if (!window.matchMedia("print").matches)
2122 ctx.clearRect(0, 0, canvas.width, canvas.height);
2123 }
2124 ctx.restore();
2125}
2126
2127function drawNets(canvas, layer, highlight) {
2128 var style = getComputedStyle(topmostdiv);
2129 if (settings.renderZones) {
2130 var zoneColor = style.getPropertyValue(highlight ? '--zone-color-highlight' : '--zone-color');
2131 drawZones(canvas, layer, zoneColor, highlight);
2132 }
2133 if (settings.renderTracks) {
2134 var trackColor = style.getPropertyValue(highlight ? '--track-color-highlight' : '--track-color');
2135 drawTracks(canvas, layer, trackColor, highlight);
2136 }
2137 if (highlight && settings.renderPads) {
2138 var padColor = style.getPropertyValue('--pad-color-highlight');
2139 var padHoleColor = style.getPropertyValue('--pad-hole-color');
2140 var ctx = canvas.getContext("2d");
2141 for (var footprint of pcbdata.footprints) {
2142 // draw pads
2143 var padDrawn = false;
2144 for (var pad of footprint.pads) {
2145 if (highlightedNet != pad.net) continue;
2146 if (pad.layers.includes(layer)) {
2147 drawPad(ctx, pad, padColor, false);
2148 padDrawn = true;
2149 }
2150 }
2151 if (padDrawn) {
2152 // redraw all pad holes because some pads may overlap
2153 for (var pad of footprint.pads) {
2154 drawPadHole(ctx, pad, padHoleColor);
2155 }
2156 }
2157 }
2158 }
2159}
2160
2161function drawHighlightsOnLayer(canvasdict, clear = true) {
2162 if (clear) {
2163 clearCanvas(canvasdict.highlight);
2164 }
2165 if (markedFootprints.size > 0 || highlightedFootprints.length > 0) {
2166 drawFootprints(canvasdict.highlight, canvasdict.layer,
2167 canvasdict.transform.s * canvasdict.transform.zoom, true);
2168 }
2169 if (highlightedNet !== null) {
2170 drawNets(canvasdict.highlight, canvasdict.layer, true);
2171 }
2172}
2173
2174function drawHighlights() {
2175 drawHighlightsOnLayer(allcanvas.front);
2176 drawHighlightsOnLayer(allcanvas.back);
2177}
2178
2179function drawBackground(canvasdict, clear = true) {
2180 if (clear) {
2181 clearCanvas(canvasdict.bg);
2182 clearCanvas(canvasdict.fab);
2183 clearCanvas(canvasdict.silk);
2184 }
2185
2186 drawNets(canvasdict.bg, canvasdict.layer, false);
2187 drawFootprints(canvasdict.bg, canvasdict.layer,
2188 canvasdict.transform.s * canvasdict.transform.zoom, false);
2189
2190 drawEdgeCuts(canvasdict.bg, canvasdict.transform.s * canvasdict.transform.zoom);
2191
2192 var style = getComputedStyle(topmostdiv);
2193 var edgeColor = style.getPropertyValue('--silkscreen-edge-color');
2194 var polygonColor = style.getPropertyValue('--silkscreen-polygon-color');
2195 var textColor = style.getPropertyValue('--silkscreen-text-color');
2196 if (settings.renderSilkscreen) {
2197 drawBgLayer(
2198 "silkscreen", canvasdict.silk, canvasdict.layer,
2199 canvasdict.transform.s * canvasdict.transform.zoom,
2200 edgeColor, polygonColor, textColor);
2201 }
2202 edgeColor = style.getPropertyValue('--fabrication-edge-color');
2203 polygonColor = style.getPropertyValue('--fabrication-polygon-color');
2204 textColor = style.getPropertyValue('--fabrication-text-color');
2205 if (settings.renderFabrication) {
2206 drawBgLayer(
2207 "fabrication", canvasdict.fab, canvasdict.layer,
2208 canvasdict.transform.s * canvasdict.transform.zoom,
2209 edgeColor, polygonColor, textColor);
2210 }
2211}
2212
2213function prepareCanvas(canvas, flip, transform) {
2214 var ctx = canvas.getContext("2d");
2215 ctx.setTransform(1, 0, 0, 1, 0, 0);
2216 ctx.scale(transform.zoom, transform.zoom);
2217 ctx.translate(transform.panx, transform.pany);
2218 if (flip) {
2219 ctx.scale(-1, 1);
2220 }
2221 ctx.translate(transform.x, transform.y);
2222 ctx.rotate(deg2rad(settings.boardRotation + (flip && settings.offsetBackRotation ? - 180 : 0)));
2223 ctx.scale(transform.s, transform.s);
2224}
2225
2226function prepareLayer(canvasdict) {
2227 var flip = (canvasdict.layer === "B");
2228 for (var c of ["bg", "fab", "silk", "highlight"]) {
2229 prepareCanvas(canvasdict[c], flip, canvasdict.transform);
2230 }
2231}
2232
2233function rotateVector(v, angle) {
2234 angle = deg2rad(angle);
2235 return [
2236 v[0] * Math.cos(angle) - v[1] * Math.sin(angle),
2237 v[0] * Math.sin(angle) + v[1] * Math.cos(angle)
2238 ];
2239}
2240
2241function applyRotation(bbox, flip) {
2242 var corners = [
2243 [bbox.minx, bbox.miny],
2244 [bbox.minx, bbox.maxy],
2245 [bbox.maxx, bbox.miny],
2246 [bbox.maxx, bbox.maxy],
2247 ];
2248 corners = corners.map((v) => rotateVector(v, settings.boardRotation + (flip && settings.offsetBackRotation ? - 180 : 0)));
2249 return {
2250 minx: corners.reduce((a, v) => Math.min(a, v[0]), Infinity),
2251 miny: corners.reduce((a, v) => Math.min(a, v[1]), Infinity),
2252 maxx: corners.reduce((a, v) => Math.max(a, v[0]), -Infinity),
2253 maxy: corners.reduce((a, v) => Math.max(a, v[1]), -Infinity),
2254 }
2255}
2256
2257function recalcLayerScale(layerdict, width, height) {
2258 var flip = (layerdict.layer === "B");
2259 var bbox = applyRotation(pcbdata.edges_bbox, flip);
2260 var scalefactor = 0.98 * Math.min(
2261 width / (bbox.maxx - bbox.minx),
2262 height / (bbox.maxy - bbox.miny)
2263 );
2264 if (scalefactor < 0.1) {
2265 scalefactor = 1;
2266 }
2267 layerdict.transform.s = scalefactor;
2268 if (flip) {
2269 layerdict.transform.x = -((bbox.maxx + bbox.minx) * scalefactor + width) * 0.5;
2270 } else {
2271 layerdict.transform.x = -((bbox.maxx + bbox.minx) * scalefactor - width) * 0.5;
2272 }
2273 layerdict.transform.y = -((bbox.maxy + bbox.miny) * scalefactor - height) * 0.5;
2274 for (var c of ["bg", "fab", "silk", "highlight"]) {
2275 canvas = layerdict[c];
2276 canvas.width = width;
2277 canvas.height = height;
2278 canvas.style.width = (width / devicePixelRatio) + "px";
2279 canvas.style.height = (height / devicePixelRatio) + "px";
2280 }
2281}
2282
2283function redrawCanvas(layerdict) {
2284 prepareLayer(layerdict);
2285 drawBackground(layerdict);
2286 drawHighlightsOnLayer(layerdict);
2287}
2288
2289function resizeCanvas(layerdict) {
2290 var canvasdivid = {
2291 "F": "frontcanvas",
2292 "B": "backcanvas"
2293 } [layerdict.layer];
2294 var width = document.getElementById(canvasdivid).clientWidth * devicePixelRatio;
2295 var height = document.getElementById(canvasdivid).clientHeight * devicePixelRatio;
2296 recalcLayerScale(layerdict, width, height);
2297 redrawCanvas(layerdict);
2298}
2299
2300function resizeAll() {
2301 resizeCanvas(allcanvas.front);
2302 resizeCanvas(allcanvas.back);
2303}
2304
2305function pointWithinDistanceToSegment(x, y, x1, y1, x2, y2, d) {
2306 var A = x - x1;
2307 var B = y - y1;
2308 var C = x2 - x1;
2309 var D = y2 - y1;
2310
2311 var dot = A * C + B * D;
2312 var len_sq = C * C + D * D;
2313 var dx, dy;
2314 if (len_sq == 0) {
2315 // start and end of the segment coincide
2316 dx = x - x1;
2317 dy = y - y1;
2318 } else {
2319 var param = dot / len_sq;
2320 var xx, yy;
2321 if (param < 0) {
2322 xx = x1;
2323 yy = y1;
2324 } else if (param > 1) {
2325 xx = x2;
2326 yy = y2;
2327 } else {
2328 xx = x1 + param * C;
2329 yy = y1 + param * D;
2330 }
2331 dx = x - xx;
2332 dy = y - yy;
2333 }
2334 return dx * dx + dy * dy <= d * d;
2335}
2336
2337function modulo(n, mod) {
2338 return ((n % mod) + mod) % mod;
2339}
2340
2341function pointWithinDistanceToArc(x, y, xc, yc, radius, startangle, endangle, d) {
2342 var dx = x - xc;
2343 var dy = y - yc;
2344 var r_sq = dx * dx + dy * dy;
2345 var rmin = Math.max(0, radius - d);
2346 var rmax = radius + d;
2347
2348 if (r_sq < rmin * rmin || r_sq > rmax * rmax)
2349 return false;
2350
2351 var angle1 = modulo(deg2rad(startangle), 2 * Math.PI);
2352 var dx1 = xc + radius * Math.cos(angle1) - x;
2353 var dy1 = yc + radius * Math.sin(angle1) - y;
2354 if (dx1 * dx1 + dy1 * dy1 <= d * d)
2355 return true;
2356
2357 var angle2 = modulo(deg2rad(endangle), 2 * Math.PI);
2358 var dx2 = xc + radius * Math.cos(angle2) - x;
2359 var dy2 = yc + radius * Math.sin(angle2) - y;
2360 if (dx2 * dx2 + dy2 * dy2 <= d * d)
2361 return true;
2362
2363 var angle = modulo(Math.atan2(dy, dx), 2 * Math.PI);
2364 if (angle1 > angle2)
2365 return (angle >= angle2 || angle <= angle1);
2366 else
2367 return (angle >= angle1 && angle <= angle2);
2368}
2369
2370function pointWithinPad(x, y, pad) {
2371 var v = [x - pad.pos[0], y - pad.pos[1]];
2372 v = rotateVector(v, pad.angle);
2373 if (pad.offset) {
2374 v[0] -= pad.offset[0];
2375 v[1] -= pad.offset[1];
2376 }
2377 return emptyContext2d.isPointInPath(getCachedPadPath(pad), ...v);
2378}
2379
2380function netHitScan(layer, x, y) {
2381 // Check track segments
2382 if (settings.renderTracks && pcbdata.tracks) {
2383 for (var track of pcbdata.tracks[layer]) {
2384 if ('radius' in track) {
2385 if (pointWithinDistanceToArc(x, y, ...track.center, track.radius, track.startangle, track.endangle, track.width / 2)) {
2386 return track.net;
2387 }
2388 } else {
2389 if (pointWithinDistanceToSegment(x, y, ...track.start, ...track.end, track.width / 2)) {
2390 return track.net;
2391 }
2392 }
2393 }
2394 }
2395 // Check pads
2396 if (settings.renderPads) {
2397 for (var footprint of pcbdata.footprints) {
2398 for (var pad of footprint.pads) {
2399 if (pad.layers.includes(layer) && pointWithinPad(x, y, pad)) {
2400 return pad.net;
2401 }
2402 }
2403 }
2404 }
2405 return null;
2406}
2407
2408function pointWithinFootprintBbox(x, y, bbox) {
2409 var v = [x - bbox.pos[0], y - bbox.pos[1]];
2410 v = rotateVector(v, bbox.angle);
2411 return bbox.relpos[0] <= v[0] && v[0] <= bbox.relpos[0] + bbox.size[0] &&
2412 bbox.relpos[1] <= v[1] && v[1] <= bbox.relpos[1] + bbox.size[1];
2413}
2414
2415function bboxHitScan(layer, x, y) {
2416 var result = [];
2417 for (var i = 0; i < pcbdata.footprints.length; i++) {
2418 var footprint = pcbdata.footprints[i];
2419 if (footprint.layer == layer) {
2420 if (pointWithinFootprintBbox(x, y, footprint.bbox)) {
2421 result.push(i);
2422 }
2423 }
2424 }
2425 return result;
2426}
2427
2428function handlePointerDown(e, layerdict) {
2429 if (e.button != 0 && e.button != 1) {
2430 return;
2431 }
2432 e.preventDefault();
2433 e.stopPropagation();
2434
2435 if (!e.hasOwnProperty("offsetX")) {
2436 // The polyfill doesn't set this properly
2437 e.offsetX = e.pageX - e.currentTarget.offsetLeft;
2438 e.offsetY = e.pageY - e.currentTarget.offsetTop;
2439 }
2440
2441 layerdict.pointerStates[e.pointerId] = {
2442 distanceTravelled: 0,
2443 lastX: e.offsetX,
2444 lastY: e.offsetY,
2445 downTime: Date.now(),
2446 };
2447}
2448
2449function handleMouseClick(e, layerdict) {
2450 if (!e.hasOwnProperty("offsetX")) {
2451 // The polyfill doesn't set this properly
2452 e.offsetX = e.pageX - e.currentTarget.offsetLeft;
2453 e.offsetY = e.pageY - e.currentTarget.offsetTop;
2454 }
2455
2456 var x = e.offsetX;
2457 var y = e.offsetY;
2458 var t = layerdict.transform;
2459 var flip = layerdict.layer === "B";
2460 if (flip) {
2461 x = (devicePixelRatio * x / t.zoom - t.panx + t.x) / -t.s;
2462 } else {
2463 x = (devicePixelRatio * x / t.zoom - t.panx - t.x) / t.s;
2464 }
2465 y = (devicePixelRatio * y / t.zoom - t.y - t.pany) / t.s;
2466 var v = rotateVector([x, y], -settings.boardRotation + (flip && settings.offsetBackRotation ? - 180 : 0));
2467 if ("nets" in pcbdata) {
2468 var net = netHitScan(layerdict.layer, ...v);
2469 if (net !== highlightedNet) {
2470 netClicked(net);
2471 }
2472 }
2473 if (highlightedNet === null) {
2474 var footprints = bboxHitScan(layerdict.layer, ...v);
2475 if (footprints.length > 0) {
2476 footprintsClicked(footprints);
2477 }
2478 }
2479}
2480
2481function handlePointerLeave(e, layerdict) {
2482 e.preventDefault();
2483 e.stopPropagation();
2484
2485 if (!settings.redrawOnDrag) {
2486 redrawCanvas(layerdict);
2487 }
2488
2489 delete layerdict.pointerStates[e.pointerId];
2490}
2491
2492function resetTransform(layerdict) {
2493 layerdict.transform.panx = 0;
2494 layerdict.transform.pany = 0;
2495 layerdict.transform.zoom = 1;
2496 redrawCanvas(layerdict);
2497}
2498
2499function handlePointerUp(e, layerdict) {
2500 if (!e.hasOwnProperty("offsetX")) {
2501 // The polyfill doesn't set this properly
2502 e.offsetX = e.pageX - e.currentTarget.offsetLeft;
2503 e.offsetY = e.pageY - e.currentTarget.offsetTop;
2504 }
2505
2506 e.preventDefault();
2507 e.stopPropagation();
2508
2509 if (e.button == 2) {
2510 // Reset pan and zoom on right click.
2511 resetTransform(layerdict);
2512 layerdict.anotherPointerTapped = false;
2513 return;
2514 }
2515
2516 // We haven't necessarily had a pointermove event since the interaction started, so make sure we update this now
2517 var ptr = layerdict.pointerStates[e.pointerId];
2518 ptr.distanceTravelled += Math.abs(e.offsetX - ptr.lastX) + Math.abs(e.offsetY - ptr.lastY);
2519
2520 if (e.button == 0 && ptr.distanceTravelled < 10 && Date.now() - ptr.downTime <= 500) {
2521 if (Object.keys(layerdict.pointerStates).length == 1) {
2522 if (layerdict.anotherPointerTapped) {
2523 // This is the second pointer coming off of a two-finger tap
2524 resetTransform(layerdict);
2525 } else {
2526 // This is just a regular tap
2527 handleMouseClick(e, layerdict);
2528 }
2529 layerdict.anotherPointerTapped = false;
2530 } else {
2531 // This is the first finger coming off of what could become a two-finger tap
2532 layerdict.anotherPointerTapped = true;
2533 }
2534 } else {
2535 if (!settings.redrawOnDrag) {
2536 redrawCanvas(layerdict);
2537 }
2538 layerdict.anotherPointerTapped = false;
2539 }
2540
2541 delete layerdict.pointerStates[e.pointerId];
2542}
2543
2544function handlePointerMove(e, layerdict) {
2545 if (!layerdict.pointerStates.hasOwnProperty(e.pointerId)) {
2546 return;
2547 }
2548 e.preventDefault();
2549 e.stopPropagation();
2550
2551 if (!e.hasOwnProperty("offsetX")) {
2552 // The polyfill doesn't set this properly
2553 e.offsetX = e.pageX - e.currentTarget.offsetLeft;
2554 e.offsetY = e.pageY - e.currentTarget.offsetTop;
2555 }
2556
2557 var thisPtr = layerdict.pointerStates[e.pointerId];
2558
2559 var dx = e.offsetX - thisPtr.lastX;
2560 var dy = e.offsetY - thisPtr.lastY;
2561
2562 // If this number is low on pointer up, we count the action as a click
2563 thisPtr.distanceTravelled += Math.abs(dx) + Math.abs(dy);
2564
2565 if (Object.keys(layerdict.pointerStates).length == 1) {
2566 // This is a simple drag
2567 layerdict.transform.panx += devicePixelRatio * dx / layerdict.transform.zoom;
2568 layerdict.transform.pany += devicePixelRatio * dy / layerdict.transform.zoom;
2569 } else if (Object.keys(layerdict.pointerStates).length == 2) {
2570 var otherPtr = Object.values(layerdict.pointerStates).filter((ptr) => ptr != thisPtr)[0];
2571
2572 var oldDist = Math.sqrt(Math.pow(thisPtr.lastX - otherPtr.lastX, 2) + Math.pow(thisPtr.lastY - otherPtr.lastY, 2));
2573 var newDist = Math.sqrt(Math.pow(e.offsetX - otherPtr.lastX, 2) + Math.pow(e.offsetY - otherPtr.lastY, 2));
2574
2575 var scaleFactor = newDist / oldDist;
2576
2577 if (scaleFactor != NaN) {
2578 layerdict.transform.zoom *= scaleFactor;
2579
2580 var zoomd = (1 - scaleFactor) / layerdict.transform.zoom;
2581 layerdict.transform.panx += devicePixelRatio * otherPtr.lastX * zoomd;
2582 layerdict.transform.pany += devicePixelRatio * otherPtr.lastY * zoomd;
2583 }
2584 }
2585
2586 thisPtr.lastX = e.offsetX;
2587 thisPtr.lastY = e.offsetY;
2588
2589 if (settings.redrawOnDrag) {
2590 redrawCanvas(layerdict);
2591 }
2592}
2593
2594function handleMouseWheel(e, layerdict) {
2595 e.preventDefault();
2596 e.stopPropagation();
2597 var t = layerdict.transform;
2598 var wheeldelta = e.deltaY;
2599 if (e.deltaMode == 1) {
2600 // FF only, scroll by lines
2601 wheeldelta *= 30;
2602 } else if (e.deltaMode == 2) {
2603 wheeldelta *= 300;
2604 }
2605 var m = Math.pow(1.1, -wheeldelta / 40);
2606 // Limit amount of zoom per tick.
2607 if (m > 2) {
2608 m = 2;
2609 } else if (m < 0.5) {
2610 m = 0.5;
2611 }
2612 t.zoom *= m;
2613 var zoomd = (1 - m) / t.zoom;
2614 t.panx += devicePixelRatio * e.offsetX * zoomd;
2615 t.pany += devicePixelRatio * e.offsetY * zoomd;
2616 redrawCanvas(layerdict);
2617}
2618
2619function addMouseHandlers(div, layerdict) {
2620 div.addEventListener("pointerdown", function(e) {
2621 handlePointerDown(e, layerdict);
2622 });
2623 div.addEventListener("pointermove", function(e) {
2624 handlePointerMove(e, layerdict);
2625 });
2626 div.addEventListener("pointerup", function(e) {
2627 handlePointerUp(e, layerdict);
2628 });
2629 var pointerleave = function(e) {
2630 handlePointerLeave(e, layerdict);
2631 }
2632 div.addEventListener("pointercancel", pointerleave);
2633 div.addEventListener("pointerleave", pointerleave);
2634 div.addEventListener("pointerout", pointerleave);
2635
2636 div.onwheel = function(e) {
2637 handleMouseWheel(e, layerdict);
2638 }
2639 for (var element of [div, layerdict.bg, layerdict.fab, layerdict.silk, layerdict.highlight]) {
2640 element.addEventListener("contextmenu", function(e) {
2641 e.preventDefault();
2642 }, false);
2643 }
2644}
2645
2646function setRedrawOnDrag(value) {
2647 settings.redrawOnDrag = value;
2648 writeStorage("redrawOnDrag", value);
2649}
2650
2651function setBoardRotation(value) {
2652 settings.boardRotation = value * 5;
2653 writeStorage("boardRotation", settings.boardRotation);
2654 document.getElementById("rotationDegree").textContent = settings.boardRotation;
2655 resizeAll();
2656}
2657
2658function setOffsetBackRotation(value) {
2659 settings.offsetBackRotation = value;
2660 writeStorage("offsetBackRotation", value);
2661 resizeAll();
2662}
2663
2664function initRender() {
2665 allcanvas = {
2666 front: {
2667 transform: {
2668 x: 0,
2669 y: 0,
2670 s: 1,
2671 panx: 0,
2672 pany: 0,
2673 zoom: 1,
2674 },
2675 pointerStates: {},
2676 anotherPointerTapped: false,
2677 bg: document.getElementById("F_bg"),
2678 fab: document.getElementById("F_fab"),
2679 silk: document.getElementById("F_slk"),
2680 highlight: document.getElementById("F_hl"),
2681 layer: "F",
2682 },
2683 back: {
2684 transform: {
2685 x: 0,
2686 y: 0,
2687 s: 1,
2688 panx: 0,
2689 pany: 0,
2690 zoom: 1,
2691 },
2692 pointerStates: {},
2693 anotherPointerTapped: false,
2694 bg: document.getElementById("B_bg"),
2695 fab: document.getElementById("B_fab"),
2696 silk: document.getElementById("B_slk"),
2697 highlight: document.getElementById("B_hl"),
2698 layer: "B",
2699 }
2700 };
2701 addMouseHandlers(document.getElementById("frontcanvas"), allcanvas.front);
2702 addMouseHandlers(document.getElementById("backcanvas"), allcanvas.back);
2703}
2704
2705///////////////////////////////////////////////
2706
2707///////////////////////////////////////////////
2708/*
2709 * Table reordering via Drag'n'Drop
2710 * Inspired by: https://htmldom.dev/drag-and-drop-table-column
2711 */
2712
2713function setBomHandlers() {
2714
2715 const bom = document.getElementById('bomtable');
2716
2717 let dragName;
2718 let placeHolderElements;
2719 let draggingElement;
2720 let forcePopulation;
2721 let xOffset;
2722 let yOffset;
2723 let wasDragged;
2724
2725 const mouseUpHandler = function(e) {
2726 // Delete dragging element
2727 draggingElement.remove();
2728
2729 // Make BOM selectable again
2730 bom.style.removeProperty("userSelect");
2731
2732 // Remove listeners
2733 document.removeEventListener('mousemove', mouseMoveHandler);
2734 document.removeEventListener('mouseup', mouseUpHandler);
2735
2736 if (wasDragged) {
2737 // Redraw whole BOM
2738 populateBomTable();
2739 }
2740 }
2741
2742 const mouseMoveHandler = function(e) {
2743 // Notice the dragging
2744 wasDragged = true;
2745
2746 // Make the dragged element visible
2747 draggingElement.style.removeProperty("display");
2748
2749 // Set elements position to mouse position
2750 draggingElement.style.left = `${e.screenX - xOffset}px`;
2751 draggingElement.style.top = `${e.screenY - yOffset}px`;
2752
2753 // Forced redrawing of BOM table
2754 if (forcePopulation) {
2755 forcePopulation = false;
2756 // Copy array
2757 phe = Array.from(placeHolderElements);
2758 // populate BOM table again
2759 populateBomHeader(dragName, phe);
2760 populateBomBody(dragName, phe);
2761 }
2762
2763 // Set up array of hidden columns
2764 var hiddenColumns = Array.from(settings.hiddenColumns);
2765 // In the ungrouped mode, quantity don't exist
2766 if (settings.bommode === "ungrouped")
2767 hiddenColumns.push("Quantity");
2768 // If no checkbox fields can be found, we consider them hidden
2769 if (settings.checkboxes.length == 0)
2770 hiddenColumns.push("checkboxes");
2771
2772 // Get table headers and group them into checkboxes, extrafields and normal headers
2773 const bh = document.getElementById("bomhead");
2774 headers = Array.from(bh.querySelectorAll("th"))
2775 headers.shift() // numCol is not part of the columnOrder
2776 headerGroups = []
2777 lastCompoundClass = null;
2778 for (i = 0; i < settings.columnOrder.length; i++) {
2779 cElem = settings.columnOrder[i];
2780 if (hiddenColumns.includes(cElem)) {
2781 // Hidden columns appear as a dummy element
2782 headerGroups.push([]);
2783 continue;
2784 }
2785 elem = headers.filter(e => getColumnOrderName(e) === cElem)[0];
2786 if (elem.classList.contains("bom-checkbox")) {
2787 if (lastCompoundClass === "bom-checkbox") {
2788 cbGroup = headerGroups.pop();
2789 cbGroup.push(elem);
2790 headerGroups.push(cbGroup);
2791 } else {
2792 lastCompoundClass = "bom-checkbox";
2793 headerGroups.push([elem])
2794 }
2795 } else {
2796 headerGroups.push([elem])
2797 }
2798 }
2799
2800 // Copy settings.columnOrder
2801 var columns = Array.from(settings.columnOrder)
2802
2803 // Set up array with indices of hidden columns
2804 var hiddenIndices = hiddenColumns.map(e => settings.columnOrder.indexOf(e));
2805 var dragIndex = columns.indexOf(dragName);
2806 var swapIndex = dragIndex;
2807 var swapDone = false;
2808
2809 // Check if the current dragged element is swapable with the left or right element
2810 if (dragIndex > 0) {
2811 // Get left headers boundingbox
2812 swapIndex = dragIndex - 1;
2813 while (hiddenIndices.includes(swapIndex) && swapIndex > 0)
2814 swapIndex--;
2815 if (!hiddenIndices.includes(swapIndex)) {
2816 box = getBoundingClientRectFromMultiple(headerGroups[swapIndex]);
2817 if (e.clientX < box.left + window.scrollX + (box.width / 2)) {
2818 swapElement = columns[dragIndex];
2819 columns.splice(dragIndex, 1);
2820 columns.splice(swapIndex, 0, swapElement);
2821 forcePopulation = true;
2822 swapDone = true;
2823 }
2824 }
2825 }
2826 if ((!swapDone) && dragIndex < headerGroups.length - 1) {
2827 // Get right headers boundingbox
2828 swapIndex = dragIndex + 1;
2829 while (hiddenIndices.includes(swapIndex))
2830 swapIndex++;
2831 if (swapIndex < headerGroups.length) {
2832 box = getBoundingClientRectFromMultiple(headerGroups[swapIndex]);
2833 if (e.clientX > box.left + window.scrollX + (box.width / 2)) {
2834 swapElement = columns[dragIndex];
2835 columns.splice(dragIndex, 1);
2836 columns.splice(swapIndex, 0, swapElement);
2837 forcePopulation = true;
2838 swapDone = true;
2839 }
2840 }
2841 }
2842
2843 // Write back change to storage
2844 if (swapDone) {
2845 settings.columnOrder = columns
2846 writeStorage("columnOrder", JSON.stringify(columns));
2847 }
2848
2849 }
2850
2851 const mouseDownHandler = function(e) {
2852 var target = e.target;
2853 if (target.tagName.toLowerCase() != "td")
2854 target = target.parentElement;
2855
2856 // Used to check if a dragging has ever happened
2857 wasDragged = false;
2858
2859 // Create new element which will be displayed as the dragged column
2860 draggingElement = document.createElement("div")
2861 draggingElement.classList.add("dragging");
2862 draggingElement.style.display = "none";
2863 draggingElement.style.position = "absolute";
2864 draggingElement.style.overflow = "hidden";
2865
2866 // Get bomhead and bombody elements
2867 const bh = document.getElementById("bomhead");
2868 const bb = document.getElementById("bombody");
2869
2870 // Get all compound headers for the current column
2871 var compoundHeaders;
2872 if (target.classList.contains("bom-checkbox")) {
2873 compoundHeaders = Array.from(bh.querySelectorAll("th.bom-checkbox"));
2874 } else {
2875 compoundHeaders = [target];
2876 }
2877
2878 // Create new table which will display the column
2879 var newTable = document.createElement("table");
2880 newTable.classList.add("bom");
2881 newTable.style.background = "white";
2882 draggingElement.append(newTable);
2883
2884 // Create new header element
2885 var newHeader = document.createElement("thead");
2886 newTable.append(newHeader);
2887
2888 // Set up array for storing all placeholder elements
2889 placeHolderElements = [];
2890
2891 // Add all compound headers to the new thead element and placeholders
2892 compoundHeaders.forEach(function(h) {
2893 clone = cloneElementWithDimensions(h);
2894 newHeader.append(clone);
2895 placeHolderElements.push(clone);
2896 });
2897
2898 // Create new body element
2899 var newBody = document.createElement("tbody");
2900 newTable.append(newBody);
2901
2902 // Get indices for compound headers
2903 var idxs = compoundHeaders.map(e => getBomTableHeaderIndex(e));
2904
2905 // For each row in the BOM body...
2906 var rows = bb.querySelectorAll("tr");
2907 rows.forEach(function(row) {
2908 // ..get the cells for the compound column
2909 const tds = row.querySelectorAll("td");
2910 var copytds = idxs.map(i => tds[i]);
2911 // Add them to the new element and the placeholders
2912 var newRow = document.createElement("tr");
2913 copytds.forEach(function(td) {
2914 clone = cloneElementWithDimensions(td);
2915 newRow.append(clone);
2916 placeHolderElements.push(clone);
2917 });
2918 newBody.append(newRow);
2919 });
2920
2921 // Compute width for compound header
2922 var width = compoundHeaders.reduce((acc, x) => acc + x.clientWidth, 0);
2923 draggingElement.style.width = `${width}px`;
2924
2925 // Insert the new dragging element and disable selection on BOM
2926 bom.insertBefore(draggingElement, null);
2927 bom.style.userSelect = "none";
2928
2929 // Determine the mouse position offset
2930 xOffset = e.screenX - compoundHeaders.reduce((acc, x) => Math.min(acc, x.offsetLeft), compoundHeaders[0].offsetLeft);
2931 yOffset = e.screenY - compoundHeaders[0].offsetTop;
2932
2933 // Get name for the column in settings.columnOrder
2934 dragName = getColumnOrderName(target);
2935
2936 // Change text and class for placeholder elements
2937 placeHolderElements = placeHolderElements.map(function(e) {
2938 newElem = cloneElementWithDimensions(e);
2939 newElem.textContent = "";
2940 newElem.classList.add("placeholder");
2941 return newElem;
2942 });
2943
2944 // On next mouse move, the whole BOM needs to be redrawn to show the placeholders
2945 forcePopulation = true;
2946
2947 // Add listeners for move and up on mouse
2948 document.addEventListener('mousemove', mouseMoveHandler);
2949 document.addEventListener('mouseup', mouseUpHandler);
2950 }
2951
2952 // In netlist mode, there is nothing to reorder
2953 if (settings.bommode === "netlist")
2954 return;
2955
2956 // Add mouseDownHandler to every column except the numCol
2957 bom.querySelectorAll("th")
2958 .forEach(function(head) {
2959 if (!head.classList.contains("numCol")) {
2960 head.onmousedown = mouseDownHandler;
2961 }
2962 });
2963
2964}
2965
2966function getBoundingClientRectFromMultiple(elements) {
2967 var elems = Array.from(elements);
2968
2969 if (elems.length == 0)
2970 return null;
2971
2972 var box = elems.shift()
2973 .getBoundingClientRect();
2974
2975 elems.forEach(function(elem) {
2976 var elembox = elem.getBoundingClientRect();
2977 box.left = Math.min(elembox.left, box.left);
2978 box.top = Math.min(elembox.top, box.top);
2979 box.width += elembox.width;
2980 box.height = Math.max(elembox.height, box.height);
2981 });
2982
2983 return box;
2984}
2985
2986function cloneElementWithDimensions(elem) {
2987 var newElem = elem.cloneNode(true);
2988 newElem.style.height = window.getComputedStyle(elem).height;
2989 newElem.style.width = window.getComputedStyle(elem).width;
2990 return newElem;
2991}
2992
2993function getBomTableHeaderIndex(elem) {
2994 const bh = document.getElementById('bomhead');
2995 const ths = Array.from(bh.querySelectorAll("th"));
2996 return ths.indexOf(elem);
2997}
2998
2999function getColumnOrderName(elem) {
3000 var cname = elem.getAttribute("col_name");
3001 if (cname === "bom-checkbox")
3002 return "checkboxes";
3003 else
3004 return cname;
3005}
3006
3007function resizableGrid(tablehead) {
3008 var cols = tablehead.firstElementChild.children;
3009 var rowWidth = tablehead.offsetWidth;
3010
3011 for (var i = 1; i < cols.length; i++) {
3012 if (cols[i].classList.contains("bom-checkbox"))
3013 continue;
3014 cols[i].style.width = ((cols[i].clientWidth - paddingDiff(cols[i])) * 100 / rowWidth) + '%';
3015 }
3016
3017 for (var i = 1; i < cols.length - 1; i++) {
3018 var div = document.createElement('div');
3019 div.className = "column-width-handle";
3020 cols[i].appendChild(div);
3021 setListeners(div);
3022 }
3023
3024 function setListeners(div) {
3025 var startX, curCol, nxtCol, curColWidth, nxtColWidth, rowWidth;
3026
3027 div.addEventListener('mousedown', function(e) {
3028 e.preventDefault();
3029 e.stopPropagation();
3030
3031 curCol = e.target.parentElement;
3032 nxtCol = curCol.nextElementSibling;
3033 startX = e.pageX;
3034
3035 var padding = paddingDiff(curCol);
3036
3037 rowWidth = curCol.parentElement.offsetWidth;
3038 curColWidth = curCol.clientWidth - padding;
3039 nxtColWidth = nxtCol.clientWidth - padding;
3040 });
3041
3042 document.addEventListener('mousemove', function(e) {
3043 if (startX) {
3044 var diffX = e.pageX - startX;
3045 diffX = -Math.min(-diffX, curColWidth - 20);
3046 diffX = Math.min(diffX, nxtColWidth - 20);
3047
3048 curCol.style.width = ((curColWidth + diffX) * 100 / rowWidth) + '%';
3049 nxtCol.style.width = ((nxtColWidth - diffX) * 100 / rowWidth) + '%';
3050 console.log(`${curColWidth + nxtColWidth} ${(curColWidth + diffX) * 100 / rowWidth + (nxtColWidth - diffX) * 100 / rowWidth}`);
3051 }
3052 });
3053
3054 document.addEventListener('mouseup', function(e) {
3055 curCol = undefined;
3056 nxtCol = undefined;
3057 startX = undefined;
3058 nxtColWidth = undefined;
3059 curColWidth = undefined
3060 });
3061 }
3062
3063 function paddingDiff(col) {
3064
3065 if (getStyleVal(col, 'box-sizing') == 'border-box') {
3066 return 0;
3067 }
3068
3069 var padLeft = getStyleVal(col, 'padding-left');
3070 var padRight = getStyleVal(col, 'padding-right');
3071 return (parseInt(padLeft) + parseInt(padRight));
3072
3073 }
3074
3075 function getStyleVal(elm, css) {
3076 return (window.getComputedStyle(elm, null).getPropertyValue(css))
3077 }
3078}
3079
3080///////////////////////////////////////////////
3081
3082///////////////////////////////////////////////
3083/* DOM manipulation and misc code */
3084
3085var bomsplit;
3086var canvassplit;
3087var initDone = false;
3088var bomSortFunction = null;
3089var currentSortColumn = null;
3090var currentSortOrder = null;
3091var currentHighlightedRowId;
3092var highlightHandlers = [];
3093var footprintIndexToHandler = {};
3094var netsToHandler = {};
3095var markedFootprints = new Set();
3096var highlightedFootprints = [];
3097var highlightedNet = null;
3098var lastClicked;
3099
3100function dbg(html) {
3101 dbgdiv.innerHTML = html;
3102}
3103
3104function redrawIfInitDone() {
3105 if (initDone) {
3106 redrawCanvas(allcanvas.front);
3107 redrawCanvas(allcanvas.back);
3108 }
3109}
3110
3111function padsVisible(value) {
3112 writeStorage("padsVisible", value);
3113 settings.renderPads = value;
3114 redrawIfInitDone();
3115}
3116
3117function referencesVisible(value) {
3118 writeStorage("referencesVisible", value);
3119 settings.renderReferences = value;
3120 redrawIfInitDone();
3121}
3122
3123function valuesVisible(value) {
3124 writeStorage("valuesVisible", value);
3125 settings.renderValues = value;
3126 redrawIfInitDone();
3127}
3128
3129function tracksVisible(value) {
3130 writeStorage("tracksVisible", value);
3131 settings.renderTracks = value;
3132 redrawIfInitDone();
3133}
3134
3135function zonesVisible(value) {
3136 writeStorage("zonesVisible", value);
3137 settings.renderZones = value;
3138 redrawIfInitDone();
3139}
3140
3141function dnpOutline(value) {
3142 writeStorage("dnpOutline", value);
3143 settings.renderDnpOutline = value;
3144 redrawIfInitDone();
3145}
3146
3147function setDarkMode(value) {
3148 if (value) {
3149 topmostdiv.classList.add("dark");
3150 } else {
3151 topmostdiv.classList.remove("dark");
3152 }
3153 writeStorage("darkmode", value);
3154 settings.darkMode = value;
3155 redrawIfInitDone();
3156 if (initDone) {
3157 populateBomTable();
3158 }
3159}
3160
3161function setShowBOMColumn(field, value) {
3162 if (field === "references") {
3163 var rl = document.getElementById("reflookup");
3164 rl.disabled = !value;
3165 if (!value) {
3166 rl.value = "";
3167 updateRefLookup("");
3168 }
3169 }
3170
3171 var n = settings.hiddenColumns.indexOf(field);
3172 if (value) {
3173 if (n != -1) {
3174 settings.hiddenColumns.splice(n, 1);
3175 }
3176 } else {
3177 if (n == -1) {
3178 settings.hiddenColumns.push(field);
3179 }
3180 }
3181
3182 writeStorage("hiddenColumns", JSON.stringify(settings.hiddenColumns));
3183
3184 if (initDone) {
3185 populateBomTable();
3186 }
3187
3188 redrawIfInitDone();
3189}
3190
3191
3192function setFullscreen(value) {
3193 if (value) {
3194 document.documentElement.requestFullscreen();
3195 } else {
3196 document.exitFullscreen();
3197 }
3198}
3199
3200function fabricationVisible(value) {
3201 writeStorage("fabricationVisible", value);
3202 settings.renderFabrication = value;
3203 redrawIfInitDone();
3204}
3205
3206function silkscreenVisible(value) {
3207 writeStorage("silkscreenVisible", value);
3208 settings.renderSilkscreen = value;
3209 redrawIfInitDone();
3210}
3211
3212function setHighlightPin1(value) {
3213 writeStorage("highlightpin1", value);
3214 settings.highlightpin1 = value;
3215 redrawIfInitDone();
3216}
3217
3218function setHighlightRowOnClick(value) {
3219 settings.highlightRowOnClick = value;
3220 writeStorage("highlightRowOnClick", value);
3221 if (initDone) {
3222 populateBomTable();
3223 }
3224}
3225
3226function getStoredCheckboxRefs(checkbox) {
3227 function convert(ref) {
3228 var intref = parseInt(ref);
3229 if (isNaN(intref)) {
3230 for (var i = 0; i < pcbdata.footprints.length; i++) {
3231 if (pcbdata.footprints[i].ref == ref) {
3232 return i;
3233 }
3234 }
3235 return -1;
3236 } else {
3237 return intref;
3238 }
3239 }
3240 if (!(checkbox in settings.checkboxStoredRefs)) {
3241 var val = readStorage("checkbox_" + checkbox);
3242 settings.checkboxStoredRefs[checkbox] = val ? val : "";
3243 }
3244 if (!settings.checkboxStoredRefs[checkbox]) {
3245 return new Set();
3246 } else {
3247 return new Set(settings.checkboxStoredRefs[checkbox].split(",").map(r => convert(r)).filter(a => a >= 0));
3248 }
3249}
3250
3251function getCheckboxState(checkbox, references) {
3252 var storedRefsSet = getStoredCheckboxRefs(checkbox);
3253 var currentRefsSet = new Set(references.map(r => r[1]));
3254 // Get difference of current - stored
3255 var difference = new Set(currentRefsSet);
3256 for (ref of storedRefsSet) {
3257 difference.delete(ref);
3258 }
3259 if (difference.size == 0) {
3260 // All the current refs are stored
3261 return "checked";
3262 } else if (difference.size == currentRefsSet.size) {
3263 // None of the current refs are stored
3264 return "unchecked";
3265 } else {
3266 // Some of the refs are stored
3267 return "indeterminate";
3268 }
3269}
3270
3271function setBomCheckboxState(checkbox, element, references) {
3272 var state = getCheckboxState(checkbox, references);
3273 element.checked = (state == "checked");
3274 element.indeterminate = (state == "indeterminate");
3275}
3276
3277function createCheckboxHandlers(input, checkbox, references, row) {
3278 var clickHandler = () => {
3279 refsSet = getStoredCheckboxRefs(checkbox);
3280 var markWhenChecked = settings.markWhenChecked == checkbox;
3281 eventArgs = {
3282 checkbox: checkbox,
3283 refs: references,
3284 }
3285 if (input.checked) {
3286 // checkbox ticked
3287 for (var ref of references) {
3288 refsSet.add(ref[1]);
3289 }
3290 if (markWhenChecked) {
3291 row.classList.add("checked");
3292 for (var ref of references) {
3293 markedFootprints.add(ref[1]);
3294 }
3295 drawHighlights();
3296 }
3297 eventArgs.state = 'checked';
3298 } else {
3299 // checkbox unticked
3300 for (var ref of references) {
3301 refsSet.delete(ref[1]);
3302 }
3303 if (markWhenChecked) {
3304 row.classList.remove("checked");
3305 for (var ref of references) {
3306 markedFootprints.delete(ref[1]);
3307 }
3308 drawHighlights();
3309 }
3310 eventArgs.state = 'unchecked';
3311 }
3312 settings.checkboxStoredRefs[checkbox] = [...refsSet].join(",");
3313 writeStorage("checkbox_" + checkbox, settings.checkboxStoredRefs[checkbox]);
3314 updateCheckboxStats(checkbox);
3315 EventHandler.emitEvent(IBOM_EVENT_TYPES.CHECKBOX_CHANGE_EVENT, eventArgs);
3316 }
3317
3318 return [
3319 (e) => {
3320 clickHandler();
3321 },
3322 (e) => {
3323 e.preventDefault();
3324 if (row.onmousemove) row.onmousemove();
3325 },
3326 (e) => {
3327 e.preventDefault();
3328 input.checked = !input.checked;
3329 input.indeterminate = false;
3330 clickHandler();
3331 }
3332 ];
3333}
3334
3335function clearHighlightedFootprints() {
3336 if (currentHighlightedRowId) {
3337 document.getElementById(currentHighlightedRowId).classList.remove("highlighted");
3338 currentHighlightedRowId = null;
3339 highlightedFootprints = [];
3340 highlightedNet = null;
3341 }
3342}
3343
3344function createRowHighlightHandler(rowid, refs, net) {
3345 return function () {
3346 if (currentHighlightedRowId) {
3347 if (currentHighlightedRowId == rowid) {
3348 return;
3349 }
3350 document.getElementById(currentHighlightedRowId).classList.remove("highlighted");
3351 }
3352 document.getElementById(rowid).classList.add("highlighted");
3353 currentHighlightedRowId = rowid;
3354 highlightedFootprints = refs ? refs.map(r => r[1]) : [];
3355 highlightedNet = net;
3356 drawHighlights();
3357 EventHandler.emitEvent(
3358 IBOM_EVENT_TYPES.HIGHLIGHT_EVENT, {
3359 rowid: rowid,
3360 refs: refs,
3361 net: net
3362 });
3363 }
3364}
3365
3366function updateNetColors() {
3367 writeStorage("netColors", JSON.stringify(settings.netColors));
3368 redrawIfInitDone();
3369}
3370
3371function netColorChangeHandler(net) {
3372 return (event) => {
3373 settings.netColors[net] = event.target.value;
3374 updateNetColors();
3375 }
3376}
3377
3378function netColorRightClick(net) {
3379 return (event) => {
3380 if (event.button == 2) {
3381 event.preventDefault();
3382 event.stopPropagation();
3383
3384 var style = getComputedStyle(topmostdiv);
3385 var defaultNetColor = style.getPropertyValue('--track-color').trim();
3386 event.target.value = defaultNetColor;
3387 delete settings.netColors[net];
3388 updateNetColors();
3389 }
3390 }
3391}
3392
3393function entryMatches(entry) {
3394 if (settings.bommode == "netlist") {
3395 // entry is just a net name
3396 return entry.toLowerCase().indexOf(filter) >= 0;
3397 }
3398 // check refs
3399 if (!settings.hiddenColumns.includes("References")) {
3400 for (var ref of entry) {
3401 if (ref[0].toLowerCase().indexOf(filter) >= 0) {
3402 return true;
3403 }
3404 }
3405 }
3406 // check fields
3407 for (var i in config.fields) {
3408 var f = config.fields[i];
3409 if (!settings.hiddenColumns.includes(f)) {
3410 for (var ref of entry) {
3411 if (String(pcbdata.bom.fields[ref[1]][i]).toLowerCase().indexOf(filter) >= 0) {
3412 return true;
3413 }
3414 }
3415 }
3416 }
3417 return false;
3418}
3419
3420function findRefInEntry(entry) {
3421 return entry.filter(r => r[0].toLowerCase() == reflookup);
3422}
3423
3424function highlightFilter(s) {
3425 if (!filter) {
3426 return s;
3427 }
3428 var parts = s.toLowerCase().split(filter);
3429 if (parts.length == 1) {
3430 return s;
3431 }
3432 var r = "";
3433 var pos = 0;
3434 for (var i in parts) {
3435 if (i > 0) {
3436 r += '<mark class="highlight">' +
3437 s.substring(pos, pos + filter.length) +
3438 '</mark>';
3439 pos += filter.length;
3440 }
3441 r += s.substring(pos, pos + parts[i].length);
3442 pos += parts[i].length;
3443 }
3444 return r;
3445}
3446
3447function getBomListByLayer(layer) {
3448 switch (layer) {
3449 case 'F': return pcbdata.bom.F.slice();
3450 case 'B': return pcbdata.bom.B.slice();
3451 case 'FB': return pcbdata.bom.both.slice();
3452 }
3453 return [];
3454}
3455
3456function getSelectedBomList() {
3457 if (settings.bommode == "netlist") {
3458 return pcbdata.nets.slice();
3459 }
3460 var out = getBomListByLayer(settings.canvaslayout);
3461
3462 if (settings.bommode == "ungrouped") {
3463 // expand bom table
3464 var expandedTable = [];
3465 for (var bomentry of out) {
3466 for (var ref of bomentry) {
3467 expandedTable.push([ref]);
3468 }
3469 }
3470 return expandedTable;
3471 }
3472
3473 return out;
3474}
3475
3476function checkboxSetUnsetAllHandler(checkboxname) {
3477 return function () {
3478 var checkboxnum = 0;
3479 while (checkboxnum < settings.checkboxes.length &&
3480 settings.checkboxes[checkboxnum].toLowerCase() != checkboxname.toLowerCase()) {
3481 checkboxnum++;
3482 }
3483 if (checkboxnum >= settings.checkboxes.length) {
3484 return;
3485 }
3486 var allset = true;
3487 var checkbox;
3488 var row;
3489 for (row of bombody.childNodes) {
3490 checkbox = row.childNodes[checkboxnum + 1].childNodes[0];
3491 if (!checkbox.checked || checkbox.indeterminate) {
3492 allset = false;
3493 break;
3494 }
3495 }
3496 for (row of bombody.childNodes) {
3497 checkbox = row.childNodes[checkboxnum + 1].childNodes[0];
3498 checkbox.checked = !allset;
3499 checkbox.indeterminate = false;
3500 checkbox.onchange();
3501 }
3502 }
3503}
3504
3505function createColumnHeader(name, cls, comparator, is_checkbox = false) {
3506 var th = document.createElement("TH");
3507 th.innerHTML = name;
3508 th.classList.add(cls);
3509 if (is_checkbox)
3510 th.setAttribute("col_name", "bom-checkbox");
3511 else
3512 th.setAttribute("col_name", name);
3513 var span = document.createElement("SPAN");
3514 span.classList.add("sortmark");
3515 span.classList.add("none");
3516 th.appendChild(span);
3517 var spacer = document.createElement("div");
3518 spacer.className = "column-spacer";
3519 th.appendChild(spacer);
3520 spacer.onclick = function () {
3521 if (currentSortColumn && th !== currentSortColumn) {
3522 // Currently sorted by another column
3523 currentSortColumn.childNodes[1].classList.remove(currentSortOrder);
3524 currentSortColumn.childNodes[1].classList.add("none");
3525 currentSortColumn = null;
3526 currentSortOrder = null;
3527 }
3528 if (currentSortColumn && th === currentSortColumn) {
3529 // Already sorted by this column
3530 if (currentSortOrder == "asc") {
3531 // Sort by this column, descending order
3532 bomSortFunction = function (a, b) {
3533 return -comparator(a, b);
3534 }
3535 currentSortColumn.childNodes[1].classList.remove("asc");
3536 currentSortColumn.childNodes[1].classList.add("desc");
3537 currentSortOrder = "desc";
3538 } else {
3539 // Unsort
3540 bomSortFunction = null;
3541 currentSortColumn.childNodes[1].classList.remove("desc");
3542 currentSortColumn.childNodes[1].classList.add("none");
3543 currentSortColumn = null;
3544 currentSortOrder = null;
3545 }
3546 } else {
3547 // Sort by this column, ascending order
3548 bomSortFunction = comparator;
3549 currentSortColumn = th;
3550 currentSortColumn.childNodes[1].classList.remove("none");
3551 currentSortColumn.childNodes[1].classList.add("asc");
3552 currentSortOrder = "asc";
3553 }
3554 populateBomBody();
3555 }
3556 if (is_checkbox) {
3557 spacer.onclick = fancyDblClickHandler(
3558 spacer, spacer.onclick, checkboxSetUnsetAllHandler(name));
3559 }
3560 return th;
3561}
3562
3563function populateBomHeader(placeHolderColumn = null, placeHolderElements = null) {
3564 while (bomhead.firstChild) {
3565 bomhead.removeChild(bomhead.firstChild);
3566 }
3567 var tr = document.createElement("TR");
3568 var th = document.createElement("TH");
3569 th.classList.add("numCol");
3570
3571 var vismenu = document.createElement("div");
3572 vismenu.id = "vismenu";
3573 vismenu.classList.add("menu");
3574
3575 var visbutton = document.createElement("div");
3576 visbutton.classList.add("visbtn");
3577 visbutton.classList.add("hideonprint");
3578
3579 var viscontent = document.createElement("div");
3580 viscontent.classList.add("menu-content");
3581 viscontent.id = "vismenu-content";
3582
3583 settings.columnOrder.forEach(column => {
3584 if (typeof column !== "string")
3585 return;
3586
3587 // Skip empty columns
3588 if (column === "checkboxes" && settings.checkboxes.length == 0)
3589 return;
3590 else if (column === "Quantity" && settings.bommode == "ungrouped")
3591 return;
3592
3593 var label = document.createElement("label");
3594 label.classList.add("menu-label");
3595
3596 var input = document.createElement("input");
3597 input.classList.add("visibility_checkbox");
3598 input.type = "checkbox";
3599 input.onchange = function (e) {
3600 setShowBOMColumn(column, e.target.checked)
3601 };
3602 input.checked = !(settings.hiddenColumns.includes(column));
3603
3604 label.appendChild(input);
3605 if (column.length > 0)
3606 label.append(column[0].toUpperCase() + column.slice(1));
3607
3608 viscontent.appendChild(label);
3609 });
3610
3611 viscontent.childNodes[0].classList.add("menu-label-top");
3612
3613 vismenu.appendChild(visbutton);
3614 if (settings.bommode != "netlist") {
3615 vismenu.appendChild(viscontent);
3616 th.appendChild(vismenu);
3617 }
3618 tr.appendChild(th);
3619
3620 var checkboxCompareClosure = function (checkbox) {
3621 return (a, b) => {
3622 var stateA = getCheckboxState(checkbox, a);
3623 var stateB = getCheckboxState(checkbox, b);
3624 if (stateA > stateB) return -1;
3625 if (stateA < stateB) return 1;
3626 return 0;
3627 }
3628 }
3629 var stringFieldCompareClosure = function (fieldIndex) {
3630 return (a, b) => {
3631 var fa = pcbdata.bom.fields[a[0][1]][fieldIndex];
3632 var fb = pcbdata.bom.fields[b[0][1]][fieldIndex];
3633 if (fa != fb) return fa > fb ? 1 : -1;
3634 else return 0;
3635 }
3636 }
3637 var referenceRegex = /(?<prefix>[^0-9]+)(?<number>[0-9]+)/;
3638 var compareRefs = (a, b) => {
3639 var ra = referenceRegex.exec(a);
3640 var rb = referenceRegex.exec(b);
3641 if (ra === null || rb === null) {
3642 if (a != b) return a > b ? 1 : -1;
3643 return 0;
3644 } else {
3645 if (ra.groups.prefix != rb.groups.prefix) {
3646 return ra.groups.prefix > rb.groups.prefix ? 1 : -1;
3647 }
3648 if (ra.groups.number != rb.groups.number) {
3649 return parseInt(ra.groups.number) > parseInt(rb.groups.number) ? 1 : -1;
3650 }
3651 return 0;
3652 }
3653 }
3654 if (settings.bommode == "netlist") {
3655 tr.appendChild(createColumnHeader("Net name", "bom-netname", (a, b) => {
3656 if (a > b) return -1;
3657 if (a < b) return 1;
3658 return 0;
3659 }));
3660 tr.appendChild(createColumnHeader("Color", "bom-color", (a, b) => {
3661 return 0;
3662 }));
3663 } else {
3664 // Filter hidden columns
3665 var columns = settings.columnOrder.filter(e => !settings.hiddenColumns.includes(e));
3666 var valueIndex = config.fields.indexOf("Value");
3667 var footprintIndex = config.fields.indexOf("Footprint");
3668 columns.forEach((column) => {
3669 if (column === placeHolderColumn) {
3670 var n = 1;
3671 if (column === "checkboxes")
3672 n = settings.checkboxes.length;
3673 for (i = 0; i < n; i++) {
3674 td = placeHolderElements.shift();
3675 tr.appendChild(td);
3676 }
3677 return;
3678 } else if (column === "checkboxes") {
3679 for (var checkbox of settings.checkboxes) {
3680 th = createColumnHeader(
3681 checkbox, "bom-checkbox", checkboxCompareClosure(checkbox), true);
3682 tr.appendChild(th);
3683 }
3684 } else if (column === "References") {
3685 tr.appendChild(createColumnHeader("References", "references", (a, b) => {
3686 var i = 0;
3687 while (i < a.length && i < b.length) {
3688 if (a[i][0] != b[i][0]) return compareRefs(a[i][0], b[i][0]);
3689 i++;
3690 }
3691 return a.length - b.length;
3692 }));
3693 } else if (column === "Value") {
3694 tr.appendChild(createColumnHeader("Value", "value", (a, b) => {
3695 var ra = a[0][1], rb = b[0][1];
3696 return valueCompare(
3697 pcbdata.bom.parsedValues[ra], pcbdata.bom.parsedValues[rb],
3698 pcbdata.bom.fields[ra][valueIndex], pcbdata.bom.fields[rb][valueIndex]);
3699 }));
3700 return;
3701 } else if (column === "Footprint") {
3702 tr.appendChild(createColumnHeader(
3703 "Footprint", "footprint", stringFieldCompareClosure(footprintIndex)));
3704 } else if (column === "Quantity" && settings.bommode == "grouped") {
3705 tr.appendChild(createColumnHeader("Quantity", "quantity", (a, b) => {
3706 return a.length - b.length;
3707 }));
3708 } else {
3709 // Other fields
3710 var i = config.fields.indexOf(column);
3711 if (i < 0)
3712 return;
3713 tr.appendChild(createColumnHeader(
3714 column, `field${i + 1}`, stringFieldCompareClosure(i)));
3715 }
3716 });
3717 }
3718 bomhead.appendChild(tr);
3719}
3720
3721function populateBomBody(placeholderColumn = null, placeHolderElements = null) {
3722 const urlRegex = /^(https?:\/\/[^\s\/$.?#][^\s]*|file:\/\/([a-zA-Z]:|\/)[^\x00]+)$/;
3723 while (bom.firstChild) {
3724 bom.removeChild(bom.firstChild);
3725 }
3726 highlightHandlers = [];
3727 footprintIndexToHandler = {};
3728 netsToHandler = {};
3729 currentHighlightedRowId = null;
3730 var first = true;
3731 var style = getComputedStyle(topmostdiv);
3732 var defaultNetColor = style.getPropertyValue('--track-color').trim();
3733
3734 bomtable = getSelectedBomList();
3735
3736 if (bomSortFunction) {
3737 bomtable = bomtable.sort(bomSortFunction);
3738 }
3739 for (var i in bomtable) {
3740 var bomentry = bomtable[i];
3741 if (filter && !entryMatches(bomentry)) {
3742 continue;
3743 }
3744 var references = null;
3745 var netname = null;
3746 var tr = document.createElement("TR");
3747 var td = document.createElement("TD");
3748 var rownum = +i + 1;
3749 tr.id = "bomrow" + rownum;
3750 td.textContent = rownum;
3751 tr.appendChild(td);
3752 if (settings.bommode == "netlist") {
3753 netname = bomentry;
3754 td = document.createElement("TD");
3755 td.innerHTML = highlightFilter(netname ? netname : "&lt;no net&gt;");
3756 tr.appendChild(td);
3757 var color = settings.netColors[netname] || defaultNetColor;
3758 td = document.createElement("TD");
3759 var colorBox = document.createElement("INPUT");
3760 colorBox.type = "color";
3761 colorBox.value = color;
3762 colorBox.onchange = netColorChangeHandler(netname);
3763 colorBox.onmouseup = netColorRightClick(netname);
3764 colorBox.oncontextmenu = (e) => e.preventDefault();
3765 td.appendChild(colorBox);
3766 td.classList.add("color-column");
3767 tr.appendChild(td);
3768 } else {
3769 if (reflookup) {
3770 references = findRefInEntry(bomentry);
3771 if (references.length == 0) {
3772 continue;
3773 }
3774 } else {
3775 references = bomentry;
3776 }
3777 // Filter hidden columns
3778 var columns = settings.columnOrder.filter(e => !settings.hiddenColumns.includes(e));
3779 columns.forEach((column) => {
3780 if (column === placeholderColumn) {
3781 var n = 1;
3782 if (column === "checkboxes")
3783 n = settings.checkboxes.length;
3784 for (i = 0; i < n; i++) {
3785 td = placeHolderElements.shift();
3786 tr.appendChild(td);
3787 }
3788 return;
3789 } else if (column === "checkboxes") {
3790 for (var checkbox of settings.checkboxes) {
3791 if (checkbox) {
3792 td = document.createElement("TD");
3793 var input = document.createElement("input");
3794 input.type = "checkbox";
3795 [input.onchange, td.ontouchstart, td.ontouchend] = createCheckboxHandlers(input, checkbox, references, tr);
3796 setBomCheckboxState(checkbox, input, references);
3797 if (input.checked && settings.markWhenChecked == checkbox) {
3798 tr.classList.add("checked");
3799 }
3800 td.appendChild(input);
3801 tr.appendChild(td);
3802 }
3803 }
3804 } else if (column === "References") {
3805 td = document.createElement("TD");
3806 td.innerHTML = highlightFilter(references.map(r => r[0]).join(", "));
3807 tr.appendChild(td);
3808 } else if (column === "Quantity" && settings.bommode == "grouped") {
3809 // Quantity
3810 td = document.createElement("TD");
3811 td.textContent = references.length;
3812 tr.appendChild(td);
3813 } else {
3814 // All the other fields
3815 var field_index = config.fields.indexOf(column)
3816 if (field_index < 0)
3817 return;
3818 var valueSet = new Set();
3819 references.map(r => r[1]).forEach((id) => valueSet.add(pcbdata.bom.fields[id][field_index]));
3820 td = document.createElement("TD");
3821 var output = new Array();
3822 for (let item of valueSet) {
3823 const visible = highlightFilter(String(item));
3824 if (typeof item === 'string' && item.match(urlRegex)) {
3825 output.push(`<a href="${item}" target="_blank">${visible}</a>`);
3826 } else {
3827 output.push(visible);
3828 }
3829 }
3830 td.innerHTML = output.join(", ");
3831 tr.appendChild(td);
3832 }
3833 });
3834 }
3835 bom.appendChild(tr);
3836 var handler = createRowHighlightHandler(tr.id, references, netname);
3837 if (settings.highlightRowOnClick) {
3838 tr.onmousedown = handler;
3839 } else {
3840 tr.onmousemove = handler;
3841 }
3842 highlightHandlers.push({
3843 id: tr.id,
3844 handler: handler,
3845 });
3846 if (references !== null) {
3847 for (var refIndex of references.map(r => r[1])) {
3848 footprintIndexToHandler[refIndex] = handler;
3849 }
3850 }
3851 if (netname !== null) {
3852 netsToHandler[netname] = handler;
3853 }
3854 if ((filter || reflookup) && first) {
3855 handler();
3856 first = false;
3857 }
3858 }
3859 EventHandler.emitEvent(
3860 IBOM_EVENT_TYPES.BOM_BODY_CHANGE_EVENT, {
3861 filter: filter,
3862 reflookup: reflookup,
3863 checkboxes: settings.checkboxes,
3864 bommode: settings.bommode,
3865 });
3866}
3867
3868function highlightPreviousRow() {
3869 if (!currentHighlightedRowId) {
3870 highlightHandlers[highlightHandlers.length - 1].handler();
3871 } else {
3872 if (highlightHandlers.length > 1 &&
3873 highlightHandlers[0].id == currentHighlightedRowId) {
3874 highlightHandlers[highlightHandlers.length - 1].handler();
3875 } else {
3876 for (var i = 0; i < highlightHandlers.length - 1; i++) {
3877 if (highlightHandlers[i + 1].id == currentHighlightedRowId) {
3878 highlightHandlers[i].handler();
3879 break;
3880 }
3881 }
3882 }
3883 }
3884 smoothScrollToRow(currentHighlightedRowId);
3885}
3886
3887function highlightNextRow() {
3888 if (!currentHighlightedRowId) {
3889 highlightHandlers[0].handler();
3890 } else {
3891 if (highlightHandlers.length > 1 &&
3892 highlightHandlers[highlightHandlers.length - 1].id == currentHighlightedRowId) {
3893 highlightHandlers[0].handler();
3894 } else {
3895 for (var i = 1; i < highlightHandlers.length; i++) {
3896 if (highlightHandlers[i - 1].id == currentHighlightedRowId) {
3897 highlightHandlers[i].handler();
3898 break;
3899 }
3900 }
3901 }
3902 }
3903 smoothScrollToRow(currentHighlightedRowId);
3904}
3905
3906function populateBomTable() {
3907 populateBomHeader();
3908 populateBomBody();
3909 setBomHandlers();
3910 resizableGrid(bomhead);
3911}
3912
3913function footprintsClicked(footprintIndexes) {
3914 var lastClickedIndex = footprintIndexes.indexOf(lastClicked);
3915 for (var i = 1; i <= footprintIndexes.length; i++) {
3916 var refIndex = footprintIndexes[(lastClickedIndex + i) % footprintIndexes.length];
3917 if (refIndex in footprintIndexToHandler) {
3918 lastClicked = refIndex;
3919 footprintIndexToHandler[refIndex]();
3920 smoothScrollToRow(currentHighlightedRowId);
3921 break;
3922 }
3923 }
3924}
3925
3926function netClicked(net) {
3927 if (net in netsToHandler) {
3928 netsToHandler[net]();
3929 smoothScrollToRow(currentHighlightedRowId);
3930 } else {
3931 clearHighlightedFootprints();
3932 highlightedNet = net;
3933 drawHighlights();
3934 }
3935}
3936
3937function updateFilter(input) {
3938 filter = input.toLowerCase();
3939 populateBomTable();
3940}
3941
3942function updateRefLookup(input) {
3943 reflookup = input.toLowerCase();
3944 populateBomTable();
3945}
3946
3947function changeCanvasLayout(layout) {
3948 document.getElementById("fl-btn").classList.remove("depressed");
3949 document.getElementById("fb-btn").classList.remove("depressed");
3950 document.getElementById("bl-btn").classList.remove("depressed");
3951 switch (layout) {
3952 case 'F':
3953 document.getElementById("fl-btn").classList.add("depressed");
3954 if (settings.bomlayout != "bom-only") {
3955 canvassplit.collapse(1);
3956 }
3957 break;
3958 case 'B':
3959 document.getElementById("bl-btn").classList.add("depressed");
3960 if (settings.bomlayout != "bom-only") {
3961 canvassplit.collapse(0);
3962 }
3963 break;
3964 default:
3965 document.getElementById("fb-btn").classList.add("depressed");
3966 if (settings.bomlayout != "bom-only") {
3967 canvassplit.setSizes([50, 50]);
3968 }
3969 }
3970 settings.canvaslayout = layout;
3971 writeStorage("canvaslayout", layout);
3972 resizeAll();
3973 changeBomMode(settings.bommode);
3974}
3975
3976function populateMetadata() {
3977 document.getElementById("title").innerHTML = pcbdata.metadata.title;
3978 document.getElementById("revision").innerHTML = "Rev: " + pcbdata.metadata.revision;
3979 document.getElementById("company").innerHTML = pcbdata.metadata.company;
3980 document.getElementById("filedate").innerHTML = pcbdata.metadata.date;
3981 if (pcbdata.metadata.title != "") {
3982 document.title = pcbdata.metadata.title + " BOM";
3983 }
3984 // Calculate board stats
3985 var fp_f = 0,
3986 fp_b = 0,
3987 pads_f = 0,
3988 pads_b = 0,
3989 pads_th = 0;
3990 for (var i = 0; i < pcbdata.footprints.length; i++) {
3991 if (pcbdata.bom.skipped.includes(i)) continue;
3992 var mod = pcbdata.footprints[i];
3993 if (mod.layer == "F") {
3994 fp_f++;
3995 } else {
3996 fp_b++;
3997 }
3998 for (var pad of mod.pads) {
3999 if (pad.type == "th") {
4000 pads_th++;
4001 } else {
4002 if (pad.layers.includes("F")) {
4003 pads_f++;
4004 }
4005 if (pad.layers.includes("B")) {
4006 pads_b++;
4007 }
4008 }
4009 }
4010 }
4011 document.getElementById("stats-components-front").innerHTML = fp_f;
4012 document.getElementById("stats-components-back").innerHTML = fp_b;
4013 document.getElementById("stats-components-total").innerHTML = fp_f + fp_b;
4014 document.getElementById("stats-groups-front").innerHTML = pcbdata.bom.F.length;
4015 document.getElementById("stats-groups-back").innerHTML = pcbdata.bom.B.length;
4016 document.getElementById("stats-groups-total").innerHTML = pcbdata.bom.both.length;
4017 document.getElementById("stats-smd-pads-front").innerHTML = pads_f;
4018 document.getElementById("stats-smd-pads-back").innerHTML = pads_b;
4019 document.getElementById("stats-smd-pads-total").innerHTML = pads_f + pads_b;
4020 document.getElementById("stats-th-pads").innerHTML = pads_th;
4021 // Update version string
4022 document.getElementById("github-link").innerHTML = "InteractiveHtmlBom&nbsp;" +
4023 /^v\d+\.\d+/.exec(pcbdata.ibom_version)[0];
4024}
4025
4026function changeBomLayout(layout) {
4027 document.getElementById("bom-btn").classList.remove("depressed");
4028 document.getElementById("lr-btn").classList.remove("depressed");
4029 document.getElementById("tb-btn").classList.remove("depressed");
4030 switch (layout) {
4031 case 'bom-only':
4032 document.getElementById("bom-btn").classList.add("depressed");
4033 if (bomsplit) {
4034 bomsplit.destroy();
4035 bomsplit = null;
4036 canvassplit.destroy();
4037 canvassplit = null;
4038 }
4039 document.getElementById("frontcanvas").style.display = "none";
4040 document.getElementById("backcanvas").style.display = "none";
4041 document.getElementById("topmostdiv").style.height = "";
4042 document.getElementById("topmostdiv").style.display = "block";
4043 break;
4044 case 'top-bottom':
4045 document.getElementById("tb-btn").classList.add("depressed");
4046 document.getElementById("frontcanvas").style.display = "";
4047 document.getElementById("backcanvas").style.display = "";
4048 document.getElementById("topmostdiv").style.height = "100%";
4049 document.getElementById("topmostdiv").style.display = "flex";
4050 document.getElementById("bomdiv").classList.remove("split-horizontal");
4051 document.getElementById("canvasdiv").classList.remove("split-horizontal");
4052 document.getElementById("frontcanvas").classList.add("split-horizontal");
4053 document.getElementById("backcanvas").classList.add("split-horizontal");
4054 if (bomsplit) {
4055 bomsplit.destroy();
4056 bomsplit = null;
4057 canvassplit.destroy();
4058 canvassplit = null;
4059 }
4060 bomsplit = Split(['#bomdiv', '#canvasdiv'], {
4061 sizes: [50, 50],
4062 onDragEnd: resizeAll,
4063 direction: "vertical",
4064 gutterSize: 5
4065 });
4066 canvassplit = Split(['#frontcanvas', '#backcanvas'], {
4067 sizes: [50, 50],
4068 gutterSize: 5,
4069 onDragEnd: resizeAll
4070 });
4071 break;
4072 case 'left-right':
4073 document.getElementById("lr-btn").classList.add("depressed");
4074 document.getElementById("frontcanvas").style.display = "";
4075 document.getElementById("backcanvas").style.display = "";
4076 document.getElementById("topmostdiv").style.height = "100%";
4077 document.getElementById("topmostdiv").style.display = "flex";
4078 document.getElementById("bomdiv").classList.add("split-horizontal");
4079 document.getElementById("canvasdiv").classList.add("split-horizontal");
4080 document.getElementById("frontcanvas").classList.remove("split-horizontal");
4081 document.getElementById("backcanvas").classList.remove("split-horizontal");
4082 if (bomsplit) {
4083 bomsplit.destroy();
4084 bomsplit = null;
4085 canvassplit.destroy();
4086 canvassplit = null;
4087 }
4088 bomsplit = Split(['#bomdiv', '#canvasdiv'], {
4089 sizes: [50, 50],
4090 onDragEnd: resizeAll,
4091 gutterSize: 5
4092 });
4093 canvassplit = Split(['#frontcanvas', '#backcanvas'], {
4094 sizes: [50, 50],
4095 gutterSize: 5,
4096 direction: "vertical",
4097 onDragEnd: resizeAll
4098 });
4099 }
4100 settings.bomlayout = layout;
4101 writeStorage("bomlayout", layout);
4102 changeCanvasLayout(settings.canvaslayout);
4103}
4104
4105function changeBomMode(mode) {
4106 document.getElementById("bom-grouped-btn").classList.remove("depressed");
4107 document.getElementById("bom-ungrouped-btn").classList.remove("depressed");
4108 document.getElementById("bom-netlist-btn").classList.remove("depressed");
4109 var chkbxs = document.getElementsByClassName("visibility_checkbox");
4110
4111 switch (mode) {
4112 case 'grouped':
4113 document.getElementById("bom-grouped-btn").classList.add("depressed");
4114 for (var i = 0; i < chkbxs.length; i++) {
4115 chkbxs[i].disabled = false;
4116 }
4117 break;
4118 case 'ungrouped':
4119 document.getElementById("bom-ungrouped-btn").classList.add("depressed");
4120 for (var i = 0; i < chkbxs.length; i++) {
4121 chkbxs[i].disabled = false;
4122 }
4123 break;
4124 case 'netlist':
4125 document.getElementById("bom-netlist-btn").classList.add("depressed");
4126 for (var i = 0; i < chkbxs.length; i++) {
4127 chkbxs[i].disabled = true;
4128 }
4129 }
4130
4131 writeStorage("bommode", mode);
4132 if (mode != settings.bommode) {
4133 settings.bommode = mode;
4134 bomSortFunction = null;
4135 currentSortColumn = null;
4136 currentSortOrder = null;
4137 clearHighlightedFootprints();
4138 }
4139 populateBomTable();
4140}
4141
4142function focusFilterField() {
4143 focusInputField(document.getElementById("filter"));
4144}
4145
4146function focusRefLookupField() {
4147 focusInputField(document.getElementById("reflookup"));
4148}
4149
4150function toggleBomCheckbox(bomrowid, checkboxnum) {
4151 if (!bomrowid || checkboxnum > settings.checkboxes.length) {
4152 return;
4153 }
4154 var bomrow = document.getElementById(bomrowid);
4155 var childNum = checkboxnum + settings.columnOrder.indexOf("checkboxes");
4156 var checkbox = bomrow.childNodes[childNum].childNodes[0];
4157 checkbox.checked = !checkbox.checked;
4158 checkbox.indeterminate = false;
4159 checkbox.onchange();
4160}
4161
4162function checkBomCheckbox(bomrowid, checkboxname) {
4163 var checkboxnum = 0;
4164 while (checkboxnum < settings.checkboxes.length &&
4165 settings.checkboxes[checkboxnum].toLowerCase() != checkboxname.toLowerCase()) {
4166 checkboxnum++;
4167 }
4168 if (!bomrowid || checkboxnum >= settings.checkboxes.length) {
4169 return;
4170 }
4171 var bomrow = document.getElementById(bomrowid);
4172 var childNum = checkboxnum + 1 + settings.columnOrder.indexOf("checkboxes");
4173 var checkbox = bomrow.childNodes[childNum].childNodes[0];
4174 checkbox.checked = true;
4175 checkbox.indeterminate = false;
4176 checkbox.onchange();
4177}
4178
4179function setBomCheckboxes(value) {
4180 writeStorage("bomCheckboxes", value);
4181 settings.checkboxes = value.split(",").map((e) => e.trim()).filter((e) => e);
4182 prepCheckboxes();
4183 populateMarkWhenCheckedOptions();
4184 setMarkWhenChecked(settings.markWhenChecked);
4185}
4186
4187function setMarkWhenChecked(value) {
4188 writeStorage("markWhenChecked", value);
4189 settings.markWhenChecked = value;
4190 markedFootprints.clear();
4191 for (var ref of (value ? getStoredCheckboxRefs(value) : [])) {
4192 markedFootprints.add(ref);
4193 }
4194 populateBomTable();
4195 drawHighlights();
4196}
4197
4198function prepCheckboxes() {
4199 var table = document.getElementById("checkbox-stats");
4200 while (table.childElementCount > 1) {
4201 table.removeChild(table.lastChild);
4202 }
4203 if (settings.checkboxes.length) {
4204 table.style.display = "";
4205 } else {
4206 table.style.display = "none";
4207 }
4208 for (var checkbox of settings.checkboxes) {
4209 var tr = document.createElement("TR");
4210 var td = document.createElement("TD");
4211 td.innerHTML = checkbox;
4212 tr.appendChild(td);
4213 td = document.createElement("TD");
4214 td.id = "checkbox-stats-" + checkbox;
4215 var progressbar = document.createElement("div");
4216 progressbar.classList.add("bar");
4217 td.appendChild(progressbar);
4218 var text = document.createElement("div");
4219 text.classList.add("text");
4220 td.appendChild(text);
4221 tr.appendChild(td);
4222 table.appendChild(tr);
4223 updateCheckboxStats(checkbox);
4224 }
4225}
4226
4227function populateMarkWhenCheckedOptions() {
4228 var container = document.getElementById("markWhenCheckedContainer");
4229
4230 if (settings.checkboxes.length == 0) {
4231 container.parentElement.style.display = "none";
4232 return;
4233 }
4234
4235 container.innerHTML = '';
4236 container.parentElement.style.display = "inline-block";
4237
4238 function createOption(name, displayName) {
4239 var id = "markWhenChecked-" + name;
4240
4241 var div = document.createElement("div");
4242 div.classList.add("radio-container");
4243
4244 var input = document.createElement("input");
4245 input.type = "radio";
4246 input.name = "markWhenChecked";
4247 input.value = name;
4248 input.id = id;
4249 input.onchange = () => setMarkWhenChecked(name);
4250 div.appendChild(input);
4251
4252 // Preserve the selected element when the checkboxes change
4253 if (name == settings.markWhenChecked) {
4254 input.checked = true;
4255 }
4256
4257 var label = document.createElement("label");
4258 label.innerHTML = displayName;
4259 label.htmlFor = id;
4260 div.appendChild(label);
4261
4262 container.appendChild(div);
4263 }
4264 createOption("", "None");
4265 for (var checkbox of settings.checkboxes) {
4266 createOption(checkbox, checkbox);
4267 }
4268}
4269
4270function updateCheckboxStats(checkbox) {
4271 var checked = getStoredCheckboxRefs(checkbox).size;
4272 var total = pcbdata.footprints.length - pcbdata.bom.skipped.length;
4273 var percent = checked * 100.0 / total;
4274 var td = document.getElementById("checkbox-stats-" + checkbox);
4275 td.firstChild.style.width = percent + "%";
4276 td.lastChild.innerHTML = checked + "/" + total + " (" + Math.round(percent) + "%)";
4277}
4278
4279function constrain(number, min, max) {
4280 return Math.min(Math.max(parseInt(number), min), max);
4281}
4282
4283document.onkeydown = function (e) {
4284 switch (e.key) {
4285 case "n":
4286 if (document.activeElement.type == "text") {
4287 return;
4288 }
4289 if (currentHighlightedRowId !== null) {
4290 checkBomCheckbox(currentHighlightedRowId, "placed");
4291 highlightNextRow();
4292 e.preventDefault();
4293 }
4294 break;
4295 case "ArrowUp":
4296 highlightPreviousRow();
4297 e.preventDefault();
4298 break;
4299 case "ArrowDown":
4300 highlightNextRow();
4301 e.preventDefault();
4302 break;
4303 case "ArrowLeft":
4304 case "ArrowRight":
4305 if (document.activeElement.type != "text") {
4306 e.preventDefault();
4307 let boardRotationElement = document.getElementById("boardRotation")
4308 settings.boardRotation = parseInt(boardRotationElement.value); // degrees / 5
4309 if (e.key == "ArrowLeft") {
4310 settings.boardRotation += 3; // 15 degrees
4311 }
4312 else {
4313 settings.boardRotation -= 3;
4314 }
4315 settings.boardRotation = constrain(settings.boardRotation, boardRotationElement.min, boardRotationElement.max);
4316 boardRotationElement.value = settings.boardRotation
4317 setBoardRotation(settings.boardRotation);
4318 }
4319 break;
4320 default:
4321 break;
4322 }
4323 if (e.altKey) {
4324 switch (e.key) {
4325 case "f":
4326 focusFilterField();
4327 e.preventDefault();
4328 break;
4329 case "r":
4330 focusRefLookupField();
4331 e.preventDefault();
4332 break;
4333 case "z":
4334 changeBomLayout("bom-only");
4335 e.preventDefault();
4336 break;
4337 case "x":
4338 changeBomLayout("left-right");
4339 e.preventDefault();
4340 break;
4341 case "c":
4342 changeBomLayout("top-bottom");
4343 e.preventDefault();
4344 break;
4345 case "v":
4346 changeCanvasLayout("F");
4347 e.preventDefault();
4348 break;
4349 case "b":
4350 changeCanvasLayout("FB");
4351 e.preventDefault();
4352 break;
4353 case "n":
4354 changeCanvasLayout("B");
4355 e.preventDefault();
4356 break;
4357 default:
4358 break;
4359 }
4360 if (e.key >= '1' && e.key <= '9') {
4361 toggleBomCheckbox(currentHighlightedRowId, parseInt(e.key));
4362 e.preventDefault();
4363 }
4364 }
4365}
4366
4367function hideNetlistButton() {
4368 document.getElementById("bom-ungrouped-btn").classList.remove("middle-button");
4369 document.getElementById("bom-ungrouped-btn").classList.add("right-most-button");
4370 document.getElementById("bom-netlist-btn").style.display = "none";
4371}
4372
4373function topToggle() {
4374 var top = document.getElementById("top");
4375 var toptoggle = document.getElementById("toptoggle");
4376 if (top.style.display === "none") {
4377 top.style.display = "flex";
4378 toptoggle.classList.remove("flipped");
4379 } else {
4380 top.style.display = "none";
4381 toptoggle.classList.add("flipped");
4382 }
4383}
4384
4385window.onload = function (e) {
4386 initRender();
4387 initStorage();
4388 initDefaults();
4389 initUtils();
4390 cleanGutters();
4391 populateMetadata();
4392 dbgdiv = document.getElementById("dbg");
4393 bom = document.getElementById("bombody");
4394 bomhead = document.getElementById("bomhead");
4395 filter = "";
4396 reflookup = "";
4397 if (!("nets" in pcbdata)) {
4398 hideNetlistButton();
4399 }
4400 initDone = true;
4401 setBomCheckboxes(document.getElementById("bomCheckboxes").value);
4402 // Triggers render
4403 changeBomLayout(settings.bomlayout);
4404
4405 // Users may leave fullscreen without touching the checkbox. Uncheck.
4406 document.addEventListener('fullscreenchange', () => {
4407 if (!document.fullscreenElement)
4408 document.getElementById('fullscreenCheckbox').checked = false;
4409 });
4410}
4411
4412window.onresize = resizeAll;
4413window.matchMedia("print").addListener(resizeAll);
4414
4415///////////////////////////////////////////////
4416
4417///////////////////////////////////////////////
4418
4419///////////////////////////////////////////////
4420 </script>
4421</head>
4422
4423<body>
4424
4425<div id="topmostdiv" class="topmostdiv">
4426 <div id="top">
4427 <div id="fileinfodiv">
4428 <table class="fileinfo">
4429 <tbody>
4430 <tr>
4431 <td id="title" class="title" style="width: 70%">
4432 Title
4433 </td>
4434 <td id="revision" class="title" style="width: 30%">
4435 Revision
4436 </td>
4437 </tr>
4438 <tr>
4439 <td id="company">
4440 Company
4441 </td>
4442 <td id="filedate">
4443 Date
4444 </td>
4445 </tr>
4446 </tbody>
4447 </table>
4448 </div>
4449 <div id="bomcontrols">
4450 <div class="hideonprint menu">
4451 <button class="menubtn"></button>
4452 <div class="menu-content">
4453 <label class="menu-label menu-label-top" style="width: calc(50% - 18px)">
4454 <input id="darkmodeCheckbox" type="checkbox" onchange="setDarkMode(this.checked)">
4455 Dark mode
4456 </label><!-- This comment eats space! All of it!
4457 --><label class="menu-label menu-label-top" style="width: calc(50% - 17px); border-left: 0;">
4458 <input id="fullscreenCheckbox" type="checkbox" onchange="setFullscreen(this.checked)">
4459 Full Screen
4460 </label>
4461 <label class="menu-label" style="width: calc(50% - 18px)">
4462 <input id="fabricationCheckbox" type="checkbox" checked onchange="fabricationVisible(this.checked)">
4463 Fab layer
4464 </label><!-- This comment eats space! All of it!
4465 --><label class="menu-label" style="width: calc(50% - 17px); border-left: 0;">
4466 <input id="silkscreenCheckbox" type="checkbox" checked onchange="silkscreenVisible(this.checked)">
4467 Silkscreen
4468 </label>
4469 <label class="menu-label" style="width: calc(50% - 18px)">
4470 <input id="referencesCheckbox" type="checkbox" checked onchange="referencesVisible(this.checked)">
4471 References
4472 </label><!-- This comment eats space! All of it!
4473 --><label class="menu-label" style="width: calc(50% - 17px); border-left: 0;">
4474 <input id="valuesCheckbox" type="checkbox" checked onchange="valuesVisible(this.checked)">
4475 Values
4476 </label>
4477 <div id="tracksAndZonesCheckboxes">
4478 <label class="menu-label" style="width: calc(50% - 18px)">
4479 <input id="tracksCheckbox" type="checkbox" checked onchange="tracksVisible(this.checked)">
4480 Tracks
4481 </label><!-- This comment eats space! All of it!
4482 --><label class="menu-label" style="width: calc(50% - 17px); border-left: 0;">
4483 <input id="zonesCheckbox" type="checkbox" checked onchange="zonesVisible(this.checked)">
4484 Zones
4485 </label>
4486 </div>
4487 <label class="menu-label" style="width: calc(50% - 18px)">
4488 <input id="padsCheckbox" type="checkbox" checked onchange="padsVisible(this.checked)">
4489 Pads
4490 </label><!-- This comment eats space! All of it!
4491 --><label class="menu-label" style="width: calc(50% - 17px); border-left: 0;">
4492 <input id="dnpOutlineCheckbox" type="checkbox" checked onchange="dnpOutline(this.checked)">
4493 DNP outlined
4494 </label>
4495 <label class="menu-label">
4496 <input id="highlightRowOnClickCheckbox" type="checkbox" checked onchange="setHighlightRowOnClick(this.checked)">
4497 Highlight row on click
4498 </label>
4499 <label class="menu-label">
4500 <input id="dragCheckbox" type="checkbox" checked onchange="setRedrawOnDrag(this.checked)">
4501 Continuous redraw on drag
4502 </label>
4503 <label class="menu-label">
4504 Highlight first pin
4505 <form id="highlightpin1">
4506 <div class="flexbox">
4507 <label>
4508 <input type="radio" name="highlightpin1" value="none" onchange="setHighlightPin1('none')">
4509 None
4510 </label>
4511 <label>
4512 <input type="radio" name="highlightpin1" value="all" onchange="setHighlightPin1('all')">
4513 All
4514 </label>
4515 <label>
4516 <input type="radio" name="highlightpin1" value="selected" onchange="setHighlightPin1('selected')">
4517 Selected
4518 </label>
4519 </div>
4520 </form>
4521 </label>
4522 <label class="menu-label">
4523 <span>Board rotation</span>
4524 <span style="float: right"><span id="rotationDegree">0</span>&#176;</span>
4525 <input id="boardRotation" type="range" min="-36" max="36" value="0" class="slider" oninput="setBoardRotation(this.value)">
4526 </label>
4527 <label class="menu-label">
4528 <input id="offsetBackRotationCheckbox" type="checkbox" onchange="setOffsetBackRotation(this.checked)">
4529 Offset back rotation
4530 </label>
4531 <label class="menu-label">
4532 <div style="margin-left: 5px">Bom checkboxes</div>
4533 <input id="bomCheckboxes" class="menu-textbox" type=text
4534 oninput="setBomCheckboxes(this.value)">
4535 </label>
4536 <label class="menu-label">
4537 <div style="margin-left: 5px">Mark when checked</div>
4538 <div id="markWhenCheckedContainer"></div>
4539 </label>
4540 <label class="menu-label">
4541 <span class="shameless-plug">
4542 <span>Created using</span>
4543 <a id="github-link" target="blank" href="https://github.com/openscopeproject/InteractiveHtmlBom">InteractiveHtmlBom</a>
4544 <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>
4545 </span>
4546 </label>
4547 </div>
4548 </div>
4549 <div class="button-container hideonprint">
4550 <button id="fl-btn" class="left-most-button" onclick="changeCanvasLayout('F')"
4551 title="Front only">F
4552 </button>
4553 <button id="fb-btn" class="middle-button" onclick="changeCanvasLayout('FB')"
4554 title="Front and Back">FB
4555 </button>
4556 <button id="bl-btn" class="right-most-button" onclick="changeCanvasLayout('B')"
4557 title="Back only">B
4558 </button>
4559 </div>
4560 <div class="button-container hideonprint">
4561 <button id="bom-btn" class="left-most-button" onclick="changeBomLayout('bom-only')"
4562 title="BOM only"></button>
4563 <button id="lr-btn" class="middle-button" onclick="changeBomLayout('left-right')"
4564 title="BOM left, drawings right"></button>
4565 <button id="tb-btn" class="right-most-button" onclick="changeBomLayout('top-bottom')"
4566 title="BOM top, drawings bot"></button>
4567 </div>
4568 <div class="button-container hideonprint">
4569 <button id="bom-grouped-btn" class="left-most-button" onclick="changeBomMode('grouped')"
4570 title="Grouped BOM"></button>
4571 <button id="bom-ungrouped-btn" class="middle-button" onclick="changeBomMode('ungrouped')"
4572 title="Ungrouped BOM"></button>
4573 <button id="bom-netlist-btn" class="right-most-button" onclick="changeBomMode('netlist')"
4574 title="Netlist"></button>
4575 </div>
4576 <div class="hideonprint menu">
4577 <button class="statsbtn"></button>
4578 <div class="menu-content">
4579 <table class="stats">
4580 <tbody>
4581 <tr>
4582 <td width="40%">Board stats</td>
4583 <td>Front</td>
4584 <td>Back</td>
4585 <td>Total</td>
4586 </tr>
4587 <tr>
4588 <td>Components</td>
4589 <td id="stats-components-front">~</td>
4590 <td id="stats-components-back">~</td>
4591 <td id="stats-components-total">~</td>
4592 </tr>
4593 <tr>
4594 <td>Groups</td>
4595 <td id="stats-groups-front">~</td>
4596 <td id="stats-groups-back">~</td>
4597 <td id="stats-groups-total">~</td>
4598 </tr>
4599 <tr>
4600 <td>SMD pads</td>
4601 <td id="stats-smd-pads-front">~</td>
4602 <td id="stats-smd-pads-back">~</td>
4603 <td id="stats-smd-pads-total">~</td>
4604 </tr>
4605 <tr>
4606 <td>TH pads</td>
4607 <td colspan=3 id="stats-th-pads">~</td>
4608 </tr>
4609 </tbody>
4610 </table>
4611 <table class="stats">
4612 <col width="40%"/><col />
4613 <tbody id="checkbox-stats">
4614 <tr>
4615 <td colspan=2 style="border-top: 0">Checkboxes</td>
4616 </tr>
4617 </tbody>
4618 </table>
4619 </div>
4620 </div>
4621 <div class="hideonprint menu">
4622 <button class="iobtn"></button>
4623 <div class="menu-content">
4624 <div class="menu-label menu-label-top">
4625 <div style="margin-left: 5px;">Save board image</div>
4626 <div class="flexbox">
4627 <input id="render-save-width" class="menu-textbox" type="text" value="1000" placeholder="Width"
4628 style="flex-grow: 1; width: 50px;" oninput="validateSaveImgDimension(this)">
4629 <span>X</span>
4630 <input id="render-save-height" class="menu-textbox" type="text" value="1000" placeholder="Height"
4631 style="flex-grow: 1; width: 50px;" oninput="validateSaveImgDimension(this)">
4632 </div>
4633 <label>
4634 <input id="render-save-transparent" type="checkbox">
4635 Transparent background
4636 </label>
4637 <div class="flexbox">
4638 <button class="savebtn" onclick="saveImage('F')">Front</button>
4639 <button class="savebtn" onclick="saveImage('B')">Back</button>
4640 </div>
4641 </div>
4642 <div class="menu-label">
4643 <span style="margin-left: 5px;">Config and checkbox state</span>
4644 <div class="flexbox">
4645 <button class="savebtn" onclick="saveSettings()">Export</button>
4646 <button class="savebtn" onclick="loadSettings()">Import</button>
4647 <button class="savebtn" onclick="resetSettings()">Reset</button>
4648 </div>
4649 </div>
4650 <div class="menu-label">
4651 <span style="margin-left: 5px;">Save bom table as</span>
4652 <div class="flexbox">
4653 <button class="savebtn" onclick="saveBomTable('csv')">csv</button>
4654 <button class="savebtn" onclick="saveBomTable('txt')">txt</button>
4655 </div>
4656 </div>
4657 </div>
4658 </div>
4659 </div>
4660 </div>
4661 <div id="topdivider">
4662 <div class="hideonprint">
4663 <div id="toptoggle" onclick="topToggle()"></div>
4664 </div>
4665 </div>
4666 <div id="bot" class="split" style="flex: 1 1">
4667 <div id="bomdiv" class="split split-horizontal">
4668 <div style="width: 100%">
4669 <input id="reflookup" class="textbox searchbox reflookup hideonprint" type="text" placeholder="Ref lookup"
4670 oninput="updateRefLookup(this.value)">
4671 <input id="filter" class="textbox searchbox filter hideonprint" type="text" placeholder="Filter"
4672 oninput="updateFilter(this.value)">
4673 <div class="button-container hideonprint" style="float: left; margin: 0;">
4674 <button id="copy" title="Copy bom table to clipboard"
4675 onclick="saveBomTable('clipboard')"></button>
4676 </div>
4677 </div>
4678 <div id="dbg"></div>
4679 <table class="bom" id="bomtable">
4680 <thead id="bomhead">
4681 </thead>
4682 <tbody id="bombody">
4683 </tbody>
4684 </table>
4685 </div>
4686 <div id="canvasdiv" class="split split-horizontal">
4687 <div id="frontcanvas" class="split" touch-action="none" style="overflow: hidden">
4688 <div style="position: relative; width: 100%; height: 100%;">
4689 <canvas id="F_bg" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
4690 <canvas id="F_fab" style="position: absolute; left: 0; top: 0; z-index: 1;"></canvas>
4691 <canvas id="F_slk" style="position: absolute; left: 0; top: 0; z-index: 2;"></canvas>
4692 <canvas id="F_hl" style="position: absolute; left: 0; top: 0; z-index: 3;"></canvas>
4693 </div>
4694 </div>
4695 <div id="backcanvas" class="split" touch-action="none" style="overflow: hidden">
4696 <div style="position: relative; width: 100%; height: 100%;">
4697 <canvas id="B_bg" style="position: absolute; left: 0; top: 0; z-index: 0;"></canvas>
4698 <canvas id="B_fab" style="position: absolute; left: 0; top: 0; z-index: 1;"></canvas>
4699 <canvas id="B_slk" style="position: absolute; left: 0; top: 0; z-index: 2;"></canvas>
4700 <canvas id="B_hl" style="position: absolute; left: 0; top: 0; z-index: 3;"></canvas>
4701 </div>
4702 </div>
4703 </div>
4704 </div>
4705</div>
4706
4707</body>
4708
4709</html>
Note: See TracBrowser for help on using the repository browser.