use std::io::Read;
use super::super::*;
use super::board_with;
#[test]
fn generates_complete_gerber_bundle_from_circuit_json() {
let circuit_json = board_with(
r#",
{ "type": "pcb_smtpad", "shape": "rect", "layer": "top", "x": 2.5, "y": 1.0,
"width": 1.2, "height": 0.6 },
{ "type": "pcb_plated_hole", "shape": "circle", "x": -5.0, "y": 0.0,
"hole_diameter": 0.8, "outer_diameter": 1.6 },
{ "type": "pcb_trace", "route": [
{ "route_type": "wire", "x": 0.0, "y": 0.0, "width": 0.2, "layer": "top" },
{ "route_type": "wire", "x": 2.5, "y": 1.0, "width": 0.2, "layer": "top" }
] }"#,
);
let result = generate_gerber_files(&circuit_json, "TestBoard");
let files = result.as_ref().expect("valid circuit");
for name in [
"Edge_Cuts.gbr",
"F_Cu.gbr",
"B_Cu.gbr",
"F_Mask.gbr",
"B_Mask.gbr",
"drill_pth.drl",
] {
assert!(files.contains_key(name), "{name}");
}
assert!(!files.contains_key("drill_npth.drl"));
let f_cu = &files["F_Cu.gbr"];
assert!(f_cu.contains("Copper,L1,Top"));
assert!(f_cu.contains("D03*"));
assert!(f_cu.contains("D01*"));
assert_zip_entry_round_trips(result.as_ref().expect("valid board"), "Edge_Cuts.gbr");
}
fn assert_zip_entry_round_trips(files: &GerberFiles, name: &str) {
let zip_bytes = create_gerber_zip(files).expect("zip creation");
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(zip_bytes)).expect("valid zip");
let mut decompressed = String::new();
archive
.by_name(name)
.expect("entry exists")
.read_to_string(&mut decompressed)
.unwrap();
assert_eq!(decompressed, files[name]);
}
#[test]
fn edge_cuts_close_an_open_outline() {
let json = r#"[{ "type": "pcb_board", "outline": [
{ "x": 0, "y": 0 }, { "x": 10, "y": 0 }, { "x": 10, "y": 5 }
] }]"#;
let edge = PcbModel::try_parse(json)
.unwrap()
.build_edge_cuts()
.unwrap();
let ops: Vec<&str> = edge.lines().filter(|l| l.starts_with('X')).collect();
assert_eq!(ops.first(), Some(&"X0Y0D02*"));
assert_eq!(ops.last(), Some(&"X0Y0D01*"));
assert_eq!(ops.len(), 4);
let rect = PcbModel::try_parse(&board_with(""))
.unwrap()
.build_edge_cuts()
.unwrap();
assert_eq!(rect.lines().filter(|l| l.ends_with("D01*")).count(), 4);
}
#[test]
fn trace_vias_get_copper_on_both_layers_without_double_drilling() {
let json = board_with(
r#",
{ "type": "pcb_via", "x": 1, "y": 1, "outer_diameter": 0.6, "hole_diameter": 0.3, "layers": ["top", "bottom"] },
{ "type": "pcb_trace", "route": [
{ "route_type": "wire", "x": 0, "y": 1, "width": 0.2, "layer": "top" },
{ "route_type": "via", "x": 1, "y": 1, "from_layer": "top", "to_layer": "bottom" },
{ "route_type": "wire", "x": 3, "y": 1, "width": 0.2, "layer": "bottom" },
{ "route_type": "via", "x": 3, "y": 1, "from_layer": "bottom", "to_layer": "top",
"outer_diameter": 0.7, "hole_diameter": 0.35 },
{ "route_type": "wire", "x": 4, "y": 1, "width": 0.2, "layer": "top" }
] }"#,
);
let files = generate_gerber_files(&json, "t").unwrap();
for layer in ["F_Cu.gbr", "B_Cu.gbr"] {
assert!(files[layer].contains("C,0.700000*%"), "{layer}");
assert!(files[layer].contains("X3000000Y1000000D03*"), "{layer}");
assert_eq!(files[layer].matches("X1000000Y1000000D03*").count(), 1);
}
let drill = &files["drill_pth.drl"];
assert_eq!(drill.matches("X1.0000Y1.0000").count(), 1);
assert_eq!(drill.matches("X3.0000Y1.0000").count(), 1);
}
#[test]
fn trace_via_without_diameters_fails_closed() {
let json = board_with(
r#",
{ "type": "pcb_trace", "route": [
{ "route_type": "wire", "x": 0, "y": 1, "width": 0.2, "layer": "top" },
{ "route_type": "via", "x": 1, "y": 1 },
{ "route_type": "wire", "x": 2, "y": 1, "width": 0.2, "layer": "bottom" }
] }"#,
);
assert!(generate_gerber_files(&json, "t").is_err());
}
#[test]
fn solder_mask_is_negative_and_copper_positive() {
let model = PcbModel::try_parse(&board_with("")).unwrap();
assert!(model
.build_mask_layer(true)
.unwrap()
.contains("%TF.FilePolarity,Negative*%"));
assert!(model
.build_copper_layer(crate::gerber::layers::CopperLayer::Top)
.unwrap()
.contains("%TF.FilePolarity,Positive*%"));
}
#[test]
fn rotated_pads_honor_ccw_rotation() {
let json = board_with(
r#",
{ "type": "pcb_smtpad", "shape": "rotated_rect", "layer": "top", "x": 0, "y": 0,
"width": 2, "height": 1, "ccw_rotation": 90 },
{ "type": "pcb_smtpad", "shape": "rotated_rect", "layer": "top", "x": 5, "y": 0,
"width": 2, "height": 1, "ccw_rotation": 45 },
{ "type": "pcb_smtpad", "shape": "rotated_pill", "layer": "top", "x": 9, "y": 0,
"width": 2, "height": 1, "radius": 0.5, "ccw_rotation": 30 }"#,
);
let cu = PcbModel::try_parse(&json)
.unwrap()
.build_copper_layer(crate::gerber::layers::CopperLayer::Top)
.unwrap();
assert!(cu.contains("R,1.000000X2.000000*%"));
assert!(cu.contains("G36*"));
assert!(cu.contains("G37*"));
assert!(cu.contains("C,1.000000*%"));
assert!(cu.contains("X8566987Y-250000D02*"));
assert!(cu.contains("X9433013Y250000D01*"));
}
#[test]
fn plated_hole_shapes_use_their_own_pad_and_drill_fields() {
let json = board_with(
r#",
{ "type": "pcb_plated_hole", "shape": "pill", "x": 0, "y": 0,
"outer_width": 3, "outer_height": 1.5, "hole_width": 2, "hole_height": 0.8 },
{ "type": "pcb_plated_hole", "shape": "circular_hole_with_rect_pad", "x": 5, "y": 0,
"hole_diameter": 1, "rect_pad_width": 2, "rect_pad_height": 1.8 }"#,
);
let files = generate_gerber_files(&json, "t").unwrap();
assert!(files["F_Cu.gbr"].contains("O,3.000000X1.500000*%"));
assert!(files["F_Cu.gbr"].contains("R,2.000000X1.800000*%"));
let drill = &files["drill_pth.drl"];
assert!(drill.contains("C0.8000"));
assert!(drill.contains("G00X-0.6000Y0.0000\nM15\nG01X0.6000Y0.0000\nM16\nG05"));
}