#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
#![warn(missing_docs)]
use jzon::{array, object, JsonValue};
trait ToJson {
fn to_json(&self) -> JsonValue;
}
impl ToJson for usize {
fn to_json(&self) -> JsonValue {
(*self).into()
}
}
impl ToJson for String {
fn to_json(&self) -> JsonValue {
self.clone().into()
}
}
impl ToJson for (f32, f32) {
fn to_json(&self) -> JsonValue {
array![self.0, self.1]
}
}
impl<T: ToJson> ToJson for Vec<T> {
fn to_json(&self) -> JsonValue {
let mut arr = array![];
for item in self {
arr.push(item.to_json()).unwrap();
}
arr
}
}
#[derive(Clone, PartialEq)]
pub enum ViewMode {
BomOnly,
LeftRight,
TopBottom,
}
impl ToJson for ViewMode {
fn to_json(&self) -> JsonValue {
match self {
ViewMode::BomOnly => "bom-only".into(),
ViewMode::LeftRight => "left-right".into(),
ViewMode::TopBottom => "top-bottom".into(),
}
}
}
#[derive(Clone, PartialEq)]
pub enum HighlightPin1Mode {
None,
Selected,
All,
}
impl ToJson for HighlightPin1Mode {
fn to_json(&self) -> JsonValue {
match self {
HighlightPin1Mode::None => "none".into(),
HighlightPin1Mode::Selected => "selected".into(),
HighlightPin1Mode::All => "all".into(),
}
}
}
#[derive(Clone, PartialEq)]
pub enum Layer {
Front,
Back,
}
impl ToJson for Layer {
fn to_json(&self) -> JsonValue {
match self {
Layer::Front => "F".into(),
Layer::Back => "B".into(),
}
}
}
#[derive(PartialEq)]
pub enum DrawingKind {
Polygon,
ReferenceText,
ValueText,
}
#[derive(PartialEq)]
pub enum DrawingLayer {
Edge,
SilkscreenFront,
SilkscreenBack,
FabricationFront,
FabricationBack,
}
#[non_exhaustive]
pub struct Drawing {
kind: DrawingKind,
layer: DrawingLayer,
svgpath: String,
width: f32,
filled: bool,
}
impl Drawing {
pub fn new(
kind: DrawingKind,
layer: DrawingLayer,
svgpath: &str,
width: f32,
filled: bool,
) -> Drawing {
Drawing {
kind,
layer,
svgpath: svgpath.to_owned(),
width,
filled,
}
}
}
impl ToJson for Drawing {
fn to_json(&self) -> JsonValue {
let mut obj = object! {
svgpath: self.svgpath.clone(),
filled: self.filled,
};
match self.kind {
DrawingKind::Polygon => {
obj["type"] = "polygon".into();
obj["width"] = self.width.into();
}
DrawingKind::ReferenceText => {
obj["thickness"] = self.width.into();
obj["ref"] = 1.into();
}
DrawingKind::ValueText => {
obj["thickness"] = self.width.into();
obj["val"] = 1.into();
}
}
obj
}
}
#[non_exhaustive]
pub struct Track {
layer: Layer,
start: (f32, f32),
end: (f32, f32),
width: f32,
net: Option<String>,
}
impl Track {
pub fn new(
layer: Layer,
start: (f32, f32),
end: (f32, f32),
width: f32,
net: Option<&str>,
) -> Track {
Track {
layer,
start,
end,
width,
net: net.map(|s| s.to_owned()),
}
}
}
impl ToJson for Track {
fn to_json(&self) -> JsonValue {
let mut obj = object! {
start: self.start.to_json(),
end: self.end.to_json(),
width: self.width,
};
if let Some(net) = &self.net {
obj["net"] = net.clone().into();
}
obj
}
}
#[non_exhaustive]
pub struct Via {
layers: Vec<Layer>,
pos: (f32, f32),
diameter: f32,
drill_diameter: f32,
net: Option<String>,
}
impl Via {
pub fn new(
layers: &[Layer],
pos: (f32, f32),
diameter: f32,
drill_diameter: f32,
net: Option<&str>,
) -> Via {
Via {
layers: layers.to_vec(),
pos,
diameter,
drill_diameter,
net: net.map(|s| s.to_owned()),
}
}
}
impl ToJson for Via {
fn to_json(&self) -> JsonValue {
let mut obj = object! {
start: self.pos.to_json(),
end: self.pos.to_json(),
width: self.diameter,
drillsize: self.drill_diameter,
};
if let Some(net) = &self.net {
obj["net"] = net.clone().into();
}
obj
}
}
#[non_exhaustive]
pub struct Zone {
layer: Layer,
svgpath: String,
net: Option<String>,
}
impl Zone {
pub fn new(layer: Layer, svgpath: &str, net: Option<&str>) -> Zone {
Zone {
layer,
svgpath: svgpath.to_owned(),
net: net.map(|s| s.to_owned()),
}
}
}
impl ToJson for Zone {
fn to_json(&self) -> JsonValue {
let mut obj = object! {
svgpath: self.svgpath.clone(),
};
if let Some(net) = &self.net {
obj["net"] = net.clone().into();
}
obj
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct Pad {
layers: Vec<Layer>,
pos: (f32, f32),
angle: f32,
svgpath: String,
drill_size: Option<(f32, f32)>,
net: Option<String>,
pin1: bool,
}
impl Pad {
pub fn new(
layers: &[Layer],
pos: (f32, f32),
angle: f32,
svgpath: &str,
drill_size: Option<(f32, f32)>,
net: Option<&str>,
pin1: bool,
) -> Pad {
Pad {
layers: layers.into(),
pos,
angle,
svgpath: svgpath.to_owned(),
drill_size,
net: net.map(|s| s.to_owned()),
pin1,
}
}
}
impl ToJson for Pad {
fn to_json(&self) -> JsonValue {
let mut obj = object! {
layers: self.layers.to_json(),
pos: self.pos.to_json(),
angle: self.angle,
shape: "custom",
svgpath: self.svgpath.clone(),
};
if let Some(drill) = &self.drill_size {
obj["type"] = "th".into();
obj["drillsize"] = array![drill.0, drill.1];
obj["drillshape"] = if drill.0 != drill.1 {
"oblong".into()
} else {
"circle".into()
};
} else {
obj["type"] = "smd".into();
}
if let Some(net) = &self.net {
obj["net"] = net.clone().into();
}
if self.pin1 {
obj["pin1"] = 1.into();
}
obj
}
}
#[non_exhaustive]
pub struct Footprint {
layer: Layer,
pos: (f32, f32),
angle: f32,
bottom_left: (f32, f32),
top_right: (f32, f32),
fields: Vec<String>,
pads: Vec<Pad>,
mount: bool,
}
impl Footprint {
#[allow(clippy::too_many_arguments)]
pub fn new(
layer: Layer,
pos: (f32, f32),
angle: f32,
bottom_left: (f32, f32),
top_right: (f32, f32),
fields: &[String],
pads: &[Pad],
mount: bool,
) -> Footprint {
Footprint {
layer,
pos,
angle,
bottom_left,
top_right,
fields: fields.to_vec(),
pads: pads.to_vec(),
mount,
}
}
}
impl ToJson for Footprint {
fn to_json(&self) -> JsonValue {
object! {
bbox: object!{
pos: self.pos.to_json(),
angle: self.angle,
relpos: self.bottom_left.to_json(),
size: array![
self.top_right.0 - self.bottom_left.0,
self.top_right.1 - self.bottom_left.1],
},
drawings: array![], layer: self.layer.to_json(),
pads: self.pads.to_json(),
}
}
}
#[derive(Clone)]
#[non_exhaustive]
pub struct RefMap {
reference: String,
footprint_id: usize,
}
impl RefMap {
pub fn new(reference: &str, footprint_id: usize) -> RefMap {
RefMap {
reference: reference.to_owned(),
footprint_id,
}
}
}
impl ToJson for RefMap {
fn to_json(&self) -> JsonValue {
array! {
self.reference.clone(),
self.footprint_id,
}
}
}
#[non_exhaustive]
pub struct InteractiveHtmlBom {
title: String,
company: String,
revision: String,
date: String,
bottom_left: (f32, f32),
top_right: (f32, f32),
pub view_mode: ViewMode,
pub highlight_pin1: HighlightPin1Mode,
pub dark_mode: bool,
pub board_rotation: f32,
pub offset_back_rotation: bool,
pub show_silkscreen: bool,
pub show_fabrication: bool,
pub show_pads: bool,
pub checkboxes: Vec<String>,
pub fields: Vec<String>,
pub user_header: String,
pub user_footer: String,
pub user_js: String,
pub drawings: Vec<Drawing>,
pub tracks: Vec<Track>,
pub vias: Vec<Via>,
pub zones: Vec<Zone>,
pub footprints: Vec<Footprint>,
pub bom_front: Vec<Vec<RefMap>>,
pub bom_back: Vec<Vec<RefMap>>,
pub bom_both: Vec<Vec<RefMap>>,
}
impl InteractiveHtmlBom {
pub fn new(
title: &str,
company: &str,
revision: &str,
date: &str,
bottom_left: (f32, f32),
top_right: (f32, f32),
) -> InteractiveHtmlBom {
InteractiveHtmlBom {
title: title.to_owned(),
revision: revision.to_owned(),
company: company.to_owned(),
date: date.to_owned(),
bottom_left,
top_right,
view_mode: ViewMode::LeftRight,
highlight_pin1: HighlightPin1Mode::None,
dark_mode: false,
board_rotation: 0.0,
offset_back_rotation: false,
show_silkscreen: true,
show_fabrication: true,
show_pads: true,
checkboxes: vec!["Sourced".into(), "Placed".into()],
fields: Vec::new(),
user_js: String::new(),
user_header: String::new(),
user_footer: String::new(),
drawings: Vec::new(),
tracks: Vec::new(),
vias: Vec::new(),
zones: Vec::new(),
footprints: Vec::new(),
bom_front: Vec::new(),
bom_back: Vec::new(),
bom_both: Vec::new(),
}
}
pub fn add_footprint(&mut self, fpt: Footprint) -> usize {
self.footprints.push(fpt);
self.footprints.len() - 1
}
pub fn generate_html(&self) -> Result<String, String> {
for bom in [&self.bom_back, &self.bom_front, &self.bom_both] {
for row in bom {
for map in row {
if map.footprint_id >= self.footprints.len() {
return Err("Invalid footprint ID.".into());
}
}
}
}
let mut nets = Vec::new();
let mut dnp_footprint_ids: Vec<usize> = Vec::new();
for (index, footprint) in self.footprints.iter().enumerate() {
if !footprint.mount {
dnp_footprint_ids.push(index);
}
for pad in &footprint.pads {
if let Some(net) = &pad.net {
if !nets.contains(net) {
nets.push(net.clone());
}
}
}
}
let layer_view = if !self.bom_front.is_empty() && self.bom_back.is_empty() {
"F"
} else if self.bom_front.is_empty() && !self.bom_back.is_empty() {
"B"
} else {
"FB"
};
let config = object! {
board_rotation: (self.board_rotation / 5.0) as i32,
bom_view: self.view_mode.to_json(),
checkboxes: self.checkboxes.join(","),
dark_mode: self.dark_mode,
fields: self.fields.to_json(),
highlight_pin1: self.highlight_pin1.to_json(),
kicad_text_formatting: false,
layer_view: layer_view,
offset_back_rotation: self.offset_back_rotation,
redraw_on_drag: true,
show_fabrication: self.show_fabrication,
show_pads: self.show_pads,
show_silkscreen: self.show_silkscreen,
};
let mut data = object! {
ibom_version: String::from_utf8_lossy(include_bytes!("web/version.txt")).to_string(),
metadata: object!{
title: self.title.clone(),
company: self.company.clone(),
revision: self.revision.clone(),
date: self.date.clone(),
},
edges_bbox: object!{
minx: self.bottom_left.0,
maxx: self.top_right.0,
miny: self.bottom_left.1,
maxy: self.top_right.1,
},
edges: self.drawings.iter()
.filter(|x| x.layer == DrawingLayer::Edge)
.map(ToJson::to_json).collect::<Vec<_>>(),
drawings: object!{
silkscreen: object!{
F: self.drawings.iter()
.filter(|x| x.layer == DrawingLayer::SilkscreenFront)
.map(ToJson::to_json).collect::<Vec<_>>(),
B: self.drawings.iter()
.filter(|x| x.layer == DrawingLayer::SilkscreenBack)
.map(ToJson::to_json).collect::<Vec<_>>(),
},
fabrication: object!{
F: self.drawings.iter()
.filter(|x| x.layer == DrawingLayer::FabricationFront)
.map(ToJson::to_json).collect::<Vec<_>>(),
B: self.drawings.iter()
.filter(|x| x.layer == DrawingLayer::FabricationBack)
.map(ToJson::to_json).collect::<Vec<_>>(),
},
},
tracks: object!{
F: self.tracks.iter()
.filter(|x| x.layer == Layer::Front)
.map(ToJson::to_json)
.chain(self.vias.iter()
.filter(|x| x.layers.contains(&Layer::Front))
.map(ToJson::to_json))
.collect::<Vec<_>>(),
B: self.tracks.iter()
.filter(|x| x.layer == Layer::Back)
.map(ToJson::to_json)
.chain(self.vias.iter()
.filter(|x| x.layers.contains(&Layer::Back))
.map(ToJson::to_json))
.collect::<Vec<_>>(),
},
zones: object!{
F: self.zones.iter()
.filter(|x| x.layer == Layer::Front)
.map(ToJson::to_json).collect::<Vec<_>>(),
B: self.zones.iter()
.filter(|x| x.layer == Layer::Back)
.map(ToJson::to_json).collect::<Vec<_>>(),
},
nets: nets.to_json(),
footprints: self.footprints.to_json(),
bom: object!{
F: self.bom_front.to_json(),
B: self.bom_back.to_json(),
both: self.bom_both.to_json(),
skipped: dnp_footprint_ids.to_json(),
fields: object!{}, },
};
for (id, fpt) in self.footprints.iter().enumerate() {
if fpt.fields.len() != self.fields.len() {
return Err("Inconsistent number of fields.".into());
}
data["bom"]["fields"][id.to_string()] = fpt.fields.to_json();
}
let config_str = "var config = ".to_owned() + &config.dump();
let pcbdata_str =
"var pcbdata = JSON.parse(LZString.decompressFromBase64(\"".to_owned()
+ &lz_str::compress_to_base64(&data.dump())
+ "\"))";
let mut html =
String::from_utf8_lossy(include_bytes!("web/ibom.html")).to_string();
let replacements = [
(
"///CSS///",
String::from_utf8_lossy(include_bytes!("web/ibom.css")),
),
(
"///SPLITJS///",
String::from_utf8_lossy(include_bytes!("web/split.js")),
),
(
"///LZ-STRING///",
String::from_utf8_lossy(include_bytes!("web/lz-string.js")),
),
(
"///POINTER_EVENTS_POLYFILL///",
String::from_utf8_lossy(include_bytes!("web/pep.js")),
),
(
"///UTILJS///",
String::from_utf8_lossy(include_bytes!("web/util.js")),
),
(
"///RENDERJS///",
String::from_utf8_lossy(include_bytes!("web/render.js")),
),
(
"///TABLEUTILJS///",
String::from_utf8_lossy(include_bytes!("web/table-util.js")),
),
(
"///IBOMJS///",
String::from_utf8_lossy(include_bytes!("web/ibom.js")),
),
("///CONFIG///", config_str.as_str().into()),
("///PCBDATA///", pcbdata_str.as_str().into()),
("///USERJS///", self.user_js.as_str().into()),
("///USERHEADER///", self.user_header.as_str().into()),
("///USERFOOTER///", self.user_footer.as_str().into()),
];
for replacement in &replacements {
html = html.replace(replacement.0, &replacement.1);
}
Ok(html)
}
}