1#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
5#![warn(missing_docs)]
6
7use jzon::{array, object, JsonValue};
8
9trait ToJson {
10 fn to_json(&self) -> JsonValue;
11}
12
13impl ToJson for usize {
14 fn to_json(&self) -> JsonValue {
15 (*self).into()
16 }
17}
18
19impl ToJson for String {
20 fn to_json(&self) -> JsonValue {
21 self.clone().into()
22 }
23}
24
25impl ToJson for (f32, f32) {
26 fn to_json(&self) -> JsonValue {
27 array![self.0, self.1]
28 }
29}
30
31impl<T: ToJson> ToJson for Vec<T> {
32 fn to_json(&self) -> JsonValue {
33 let mut arr = array![];
34 for item in self {
35 arr.push(item.to_json()).unwrap();
36 }
37 arr
38 }
39}
40
41#[derive(Clone, PartialEq)]
43pub enum ViewMode {
44 BomOnly,
46 LeftRight,
48 TopBottom,
50}
51
52impl ToJson for ViewMode {
53 fn to_json(&self) -> JsonValue {
54 match self {
55 ViewMode::BomOnly => "bom-only".into(),
56 ViewMode::LeftRight => "left-right".into(),
57 ViewMode::TopBottom => "top-bottom".into(),
58 }
59 }
60}
61
62#[derive(Clone, PartialEq)]
64pub enum HighlightPin1Mode {
65 None,
67 Selected,
69 All,
71}
72
73impl ToJson for HighlightPin1Mode {
74 fn to_json(&self) -> JsonValue {
75 match self {
76 HighlightPin1Mode::None => "none".into(),
77 HighlightPin1Mode::Selected => "selected".into(),
78 HighlightPin1Mode::All => "all".into(),
79 }
80 }
81}
82
83#[derive(Clone, PartialEq)]
85pub enum Layer {
86 Front,
88 Back,
90}
91
92impl ToJson for Layer {
93 fn to_json(&self) -> JsonValue {
94 match self {
95 Layer::Front => "F".into(),
96 Layer::Back => "B".into(),
97 }
98 }
99}
100
101#[derive(PartialEq)]
103pub enum DrawingKind {
104 Polygon,
106 ReferenceText,
108 ValueText,
110}
111
112#[derive(PartialEq)]
114pub enum DrawingLayer {
115 Edge,
117 SilkscreenFront,
119 SilkscreenBack,
121 FabricationFront,
123 FabricationBack,
125}
126
127#[non_exhaustive]
129pub struct Drawing {
130 kind: DrawingKind,
131 layer: DrawingLayer,
132 svgpath: String,
133 width: f32,
134 filled: bool,
135}
136
137impl Drawing {
138 pub fn new(
152 kind: DrawingKind,
153 layer: DrawingLayer,
154 svgpath: &str,
155 width: f32,
156 filled: bool,
157 ) -> Drawing {
158 Drawing {
159 kind,
160 layer,
161 svgpath: svgpath.to_owned(),
162 width,
163 filled,
164 }
165 }
166}
167
168impl ToJson for Drawing {
169 fn to_json(&self) -> JsonValue {
170 let mut obj = object! {
171 svgpath: self.svgpath.clone(),
172 filled: self.filled,
173 };
174 match self.kind {
175 DrawingKind::Polygon => {
176 obj["type"] = "polygon".into();
177 obj["width"] = self.width.into();
178 }
179 DrawingKind::ReferenceText => {
180 obj["thickness"] = self.width.into();
181 obj["ref"] = 1.into();
182 }
183 DrawingKind::ValueText => {
184 obj["thickness"] = self.width.into();
185 obj["val"] = 1.into();
186 }
187 }
188 obj
189 }
190}
191
192#[non_exhaustive]
194pub struct Track {
195 layer: Layer,
196 start: (f32, f32),
197 end: (f32, f32),
198 width: f32,
199 net: Option<String>,
200}
201
202impl Track {
203 pub fn new(
217 layer: Layer,
218 start: (f32, f32),
219 end: (f32, f32),
220 width: f32,
221 net: Option<&str>,
222 ) -> Track {
223 Track {
224 layer,
225 start,
226 end,
227 width,
228 net: net.map(|s| s.to_owned()),
229 }
230 }
231}
232
233impl ToJson for Track {
234 fn to_json(&self) -> JsonValue {
235 let mut obj = object! {
236 start: self.start.to_json(),
237 end: self.end.to_json(),
238 width: self.width,
239 };
240 if let Some(net) = &self.net {
241 obj["net"] = net.clone().into();
242 }
243 obj
244 }
245}
246
247#[non_exhaustive]
249pub struct Via {
250 layers: Vec<Layer>,
251 pos: (f32, f32),
252 diameter: f32,
253 drill_diameter: f32,
254 net: Option<String>,
255}
256
257impl Via {
258 pub fn new(
272 layers: &[Layer],
273 pos: (f32, f32),
274 diameter: f32,
275 drill_diameter: f32,
276 net: Option<&str>,
277 ) -> Via {
278 Via {
279 layers: layers.to_vec(),
280 pos,
281 diameter,
282 drill_diameter,
283 net: net.map(|s| s.to_owned()),
284 }
285 }
286}
287
288impl ToJson for Via {
289 fn to_json(&self) -> JsonValue {
290 let mut obj = object! {
291 start: self.pos.to_json(),
292 end: self.pos.to_json(),
293 width: self.diameter,
294 drillsize: self.drill_diameter,
295 };
296 if let Some(net) = &self.net {
297 obj["net"] = net.clone().into();
298 }
299 obj
300 }
301}
302
303#[non_exhaustive]
305pub struct Zone {
306 layer: Layer,
307 svgpath: String,
308 net: Option<String>,
309}
310
311impl Zone {
312 pub fn new(layer: Layer, svgpath: &str, net: Option<&str>) -> Zone {
324 Zone {
325 layer,
326 svgpath: svgpath.to_owned(),
327 net: net.map(|s| s.to_owned()),
328 }
329 }
330}
331
332impl ToJson for Zone {
333 fn to_json(&self) -> JsonValue {
334 let mut obj = object! {
335 svgpath: self.svgpath.clone(),
336 };
337 if let Some(net) = &self.net {
338 obj["net"] = net.clone().into();
339 }
340 obj
341 }
342}
343
344#[derive(Clone)]
346#[non_exhaustive]
347pub struct Pad {
348 layers: Vec<Layer>,
349 pos: (f32, f32),
350 angle: f32,
351 svgpath: String,
352 drill_size: Option<(f32, f32)>,
353 net: Option<String>,
354 pin1: bool,
355}
356
357impl Pad {
358 pub fn new(
374 layers: &[Layer],
375 pos: (f32, f32),
376 angle: f32,
377 svgpath: &str,
378 drill_size: Option<(f32, f32)>,
379 net: Option<&str>,
380 pin1: bool,
381 ) -> Pad {
382 Pad {
383 layers: layers.into(),
384 pos,
385 angle,
386 svgpath: svgpath.to_owned(),
387 drill_size,
388 net: net.map(|s| s.to_owned()),
389 pin1,
390 }
391 }
392}
393
394impl ToJson for Pad {
395 fn to_json(&self) -> JsonValue {
396 let mut obj = object! {
397 layers: self.layers.to_json(),
398 pos: self.pos.to_json(),
399 angle: self.angle,
400 shape: "custom",
401 svgpath: self.svgpath.clone(),
402 };
403 if let Some(drill) = &self.drill_size {
404 obj["type"] = "th".into();
405 obj["drillsize"] = array![drill.0, drill.1];
406 obj["drillshape"] = if drill.0 != drill.1 {
407 "oblong".into()
408 } else {
409 "circle".into()
410 };
411 } else {
412 obj["type"] = "smd".into();
413 }
414 if let Some(net) = &self.net {
415 obj["net"] = net.clone().into();
416 }
417 if self.pin1 {
418 obj["pin1"] = 1.into();
419 }
420 obj
421 }
422}
423
424#[non_exhaustive]
426pub struct Footprint {
427 layer: Layer,
428 pos: (f32, f32),
429 angle: f32,
430 bottom_left: (f32, f32),
431 top_right: (f32, f32),
432 fields: Vec<String>,
433 pads: Vec<Pad>,
434 mount: bool,
435}
436
437impl Footprint {
438 #[allow(clippy::too_many_arguments)]
455 pub fn new(
456 layer: Layer,
457 pos: (f32, f32),
458 angle: f32,
459 bottom_left: (f32, f32),
460 top_right: (f32, f32),
461 fields: &[String],
462 pads: &[Pad],
463 mount: bool,
464 ) -> Footprint {
465 Footprint {
466 layer,
467 pos,
468 angle,
469 bottom_left,
470 top_right,
471 fields: fields.to_vec(),
472 pads: pads.to_vec(),
473 mount,
474 }
475 }
476}
477
478impl ToJson for Footprint {
479 fn to_json(&self) -> JsonValue {
480 object! {
481 bbox: object!{
482 pos: self.pos.to_json(),
483 angle: self.angle,
484 relpos: self.bottom_left.to_json(),
485 size: array![
486 self.top_right.0 - self.bottom_left.0,
487 self.top_right.1 - self.bottom_left.1],
488 },
489 drawings: array![], layer: self.layer.to_json(),
491 pads: self.pads.to_json(),
492 }
493 }
494}
495
496#[derive(Clone)]
498#[non_exhaustive]
499pub struct RefMap {
500 reference: String,
501 footprint_id: usize,
502}
503
504impl RefMap {
505 pub fn new(reference: &str, footprint_id: usize) -> RefMap {
517 RefMap {
518 reference: reference.to_owned(),
519 footprint_id,
520 }
521 }
522}
523
524impl ToJson for RefMap {
525 fn to_json(&self) -> JsonValue {
526 array! {
527 self.reference.clone(),
528 self.footprint_id,
529 }
530 }
531}
532
533#[non_exhaustive]
606pub struct InteractiveHtmlBom {
607 title: String,
609 company: String,
610 revision: String,
611 date: String,
612 bottom_left: (f32, f32),
613 top_right: (f32, f32),
614
615 pub view_mode: ViewMode,
617
618 pub highlight_pin1: HighlightPin1Mode,
620
621 pub dark_mode: bool,
623
624 pub board_rotation: f32,
626
627 pub offset_back_rotation: bool,
629
630 pub show_silkscreen: bool,
632
633 pub show_fabrication: bool,
635
636 pub show_pads: bool,
638
639 pub checkboxes: Vec<String>,
641
642 pub fields: Vec<String>,
644
645 pub user_header: String,
652
653 pub user_footer: String,
660
661 pub user_js: String,
668
669 pub drawings: Vec<Drawing>,
671
672 pub tracks: Vec<Track>,
674
675 pub vias: Vec<Via>,
677
678 pub zones: Vec<Zone>,
680
681 pub footprints: Vec<Footprint>,
683
684 pub bom_front: Vec<Vec<RefMap>>,
686
687 pub bom_back: Vec<Vec<RefMap>>,
689
690 pub bom_both: Vec<Vec<RefMap>>,
692}
693
694impl InteractiveHtmlBom {
695 pub fn new(
710 title: &str,
711 company: &str,
712 revision: &str,
713 date: &str,
714 bottom_left: (f32, f32),
715 top_right: (f32, f32),
716 ) -> InteractiveHtmlBom {
717 InteractiveHtmlBom {
718 title: title.to_owned(),
719 revision: revision.to_owned(),
720 company: company.to_owned(),
721 date: date.to_owned(),
722 bottom_left,
723 top_right,
724 view_mode: ViewMode::LeftRight,
725 highlight_pin1: HighlightPin1Mode::None,
726 dark_mode: false,
727 board_rotation: 0.0,
728 offset_back_rotation: false,
729 show_silkscreen: true,
730 show_fabrication: true,
731 show_pads: true,
732 checkboxes: vec!["Sourced".into(), "Placed".into()],
733 fields: Vec::new(),
734 user_js: String::new(),
735 user_header: String::new(),
736 user_footer: String::new(),
737 drawings: Vec::new(),
738 tracks: Vec::new(),
739 vias: Vec::new(),
740 zones: Vec::new(),
741 footprints: Vec::new(),
742 bom_front: Vec::new(),
743 bom_back: Vec::new(),
744 bom_both: Vec::new(),
745 }
746 }
747
748 pub fn add_footprint(&mut self, fpt: Footprint) -> usize {
759 self.footprints.push(fpt);
760 self.footprints.len() - 1
761 }
762
763 pub fn generate_html(&self) -> Result<String, String> {
765 for bom in [&self.bom_back, &self.bom_front, &self.bom_both] {
767 for row in bom {
768 for map in row {
769 if map.footprint_id >= self.footprints.len() {
770 return Err("Invalid footprint ID.".into());
771 }
772 }
773 }
774 }
775
776 let mut nets = Vec::new();
778 let mut dnp_footprint_ids: Vec<usize> = Vec::new();
779 for (index, footprint) in self.footprints.iter().enumerate() {
780 if !footprint.mount {
781 dnp_footprint_ids.push(index);
782 }
783 for pad in &footprint.pads {
784 if let Some(net) = &pad.net {
785 if !nets.contains(net) {
786 nets.push(net.clone());
787 }
788 }
789 }
790 }
791
792 let layer_view = if !self.bom_front.is_empty() && self.bom_back.is_empty() {
794 "F"
795 } else if self.bom_front.is_empty() && !self.bom_back.is_empty() {
796 "B"
797 } else {
798 "FB"
799 };
800
801 let config = object! {
802 board_rotation: (self.board_rotation / 5.0) as i32,
803 bom_view: self.view_mode.to_json(),
804 checkboxes: self.checkboxes.join(","),
805 dark_mode: self.dark_mode,
806 fields: self.fields.to_json(),
807 highlight_pin1: self.highlight_pin1.to_json(),
808 kicad_text_formatting: false,
809 layer_view: layer_view,
810 offset_back_rotation: self.offset_back_rotation,
811 redraw_on_drag: true,
812 show_fabrication: self.show_fabrication,
813 show_pads: self.show_pads,
814 show_silkscreen: self.show_silkscreen,
815 };
816
817 let mut data = object! {
818 ibom_version: String::from_utf8_lossy(include_bytes!("web/version.txt")).to_string(),
819 metadata: object!{
820 title: self.title.clone(),
821 company: self.company.clone(),
822 revision: self.revision.clone(),
823 date: self.date.clone(),
824 },
825 edges_bbox: object!{
826 minx: self.bottom_left.0,
827 maxx: self.top_right.0,
828 miny: self.bottom_left.1,
829 maxy: self.top_right.1,
830 },
831 edges: self.drawings.iter()
832 .filter(|x| x.layer == DrawingLayer::Edge)
833 .map(ToJson::to_json).collect::<Vec<_>>(),
834 drawings: object!{
835 silkscreen: object!{
836 F: self.drawings.iter()
837 .filter(|x| x.layer == DrawingLayer::SilkscreenFront)
838 .map(ToJson::to_json).collect::<Vec<_>>(),
839 B: self.drawings.iter()
840 .filter(|x| x.layer == DrawingLayer::SilkscreenBack)
841 .map(ToJson::to_json).collect::<Vec<_>>(),
842 },
843 fabrication: object!{
844 F: self.drawings.iter()
845 .filter(|x| x.layer == DrawingLayer::FabricationFront)
846 .map(ToJson::to_json).collect::<Vec<_>>(),
847 B: self.drawings.iter()
848 .filter(|x| x.layer == DrawingLayer::FabricationBack)
849 .map(ToJson::to_json).collect::<Vec<_>>(),
850 },
851 },
852 tracks: object!{
853 F: self.tracks.iter()
854 .filter(|x| x.layer == Layer::Front)
855 .map(ToJson::to_json)
856 .chain(self.vias.iter()
857 .filter(|x| x.layers.contains(&Layer::Front))
858 .map(ToJson::to_json))
859 .collect::<Vec<_>>(),
860 B: self.tracks.iter()
861 .filter(|x| x.layer == Layer::Back)
862 .map(ToJson::to_json)
863 .chain(self.vias.iter()
864 .filter(|x| x.layers.contains(&Layer::Back))
865 .map(ToJson::to_json))
866 .collect::<Vec<_>>(),
867 },
868 zones: object!{
869 F: self.zones.iter()
870 .filter(|x| x.layer == Layer::Front)
871 .map(ToJson::to_json).collect::<Vec<_>>(),
872 B: self.zones.iter()
873 .filter(|x| x.layer == Layer::Back)
874 .map(ToJson::to_json).collect::<Vec<_>>(),
875 },
876 nets: nets.to_json(),
877 footprints: self.footprints.to_json(),
878 bom: object!{
879 F: self.bom_front.to_json(),
880 B: self.bom_back.to_json(),
881 both: self.bom_both.to_json(),
882 skipped: dnp_footprint_ids.to_json(),
883 fields: object!{}, },
885 };
886
887 for (id, fpt) in self.footprints.iter().enumerate() {
889 if fpt.fields.len() != self.fields.len() {
890 return Err("Inconsistent number of fields.".into());
891 }
892 data["bom"]["fields"][id.to_string()] = fpt.fields.to_json();
893 }
894
895 let config_str = "var config = ".to_owned() + &config.dump();
897 let pcbdata_str =
898 "var pcbdata = JSON.parse(LZString.decompressFromBase64(\"".to_owned()
899 + &lz_str::compress_to_base64(&data.dump())
900 + "\"))";
901
902 let mut html =
904 String::from_utf8_lossy(include_bytes!("web/ibom.html")).to_string();
905
906 let replacements = [
908 (
909 "///CSS///",
910 String::from_utf8_lossy(include_bytes!("web/ibom.css")),
911 ),
912 (
913 "///SPLITJS///",
914 String::from_utf8_lossy(include_bytes!("web/split.js")),
915 ),
916 (
917 "///LZ-STRING///",
918 String::from_utf8_lossy(include_bytes!("web/lz-string.js")),
919 ),
920 (
921 "///POINTER_EVENTS_POLYFILL///",
922 String::from_utf8_lossy(include_bytes!("web/pep.js")),
923 ),
924 (
925 "///UTILJS///",
926 String::from_utf8_lossy(include_bytes!("web/util.js")),
927 ),
928 (
929 "///RENDERJS///",
930 String::from_utf8_lossy(include_bytes!("web/render.js")),
931 ),
932 (
933 "///TABLEUTILJS///",
934 String::from_utf8_lossy(include_bytes!("web/table-util.js")),
935 ),
936 (
937 "///IBOMJS///",
938 String::from_utf8_lossy(include_bytes!("web/ibom.js")),
939 ),
940 ("///CONFIG///", config_str.as_str().into()),
941 ("///PCBDATA///", pcbdata_str.as_str().into()),
942 ("///USERJS///", self.user_js.as_str().into()),
943 ("///USERHEADER///", self.user_header.as_str().into()),
944 ("///USERFOOTER///", self.user_footer.as_str().into()),
945 ];
946 for replacement in &replacements {
947 html = html.replace(replacement.0, &replacement.1);
948 }
949 Ok(html)
950 }
951}