//! Serializes geometry to gerber files. use crate::InnerAtom; use conv::TryFrom; use geo::{Point, Polygon}; use gerber_types::*; use std::collections::HashMap; const VERSION: &'static str = env!("CARGO_PKG_VERSION"); #[derive(Debug, Clone, Copy)] enum ApertureType { Circle(f64), Rect(f64, f64), } fn gerber_prelude<'a>( cf: CoordinateFormat, ff: Option, apertures: impl Iterator, ) -> Vec { let mut commands = vec![ FunctionCode::GCode(GCode::Comment("Autogenerated Maker Panel".to_string())).into(), ExtendedCode::CoordinateFormat(cf).into(), ExtendedCode::Unit(Unit::Millimeters).into(), ExtendedCode::FileAttribute(FileAttribute::GenerationSoftware( GenerationSoftware::new("Maker Panel", "maker-panel", Some(VERSION)), )) .into(), ExtendedCode::FileAttribute(FileAttribute::Part(Part::Single)).into(), if let Some(ff) = ff { ExtendedCode::FileAttribute(FileAttribute::FileFunction(ff)).into() } else { FunctionCode::GCode(GCode::Comment("".to_string())).into() }, ExtendedCode::LoadPolarity(Polarity::Dark).into(), FunctionCode::GCode(GCode::InterpolationMode(InterpolationMode::Linear)).into(), ]; for (code, shape) in apertures { commands.push( ExtendedCode::ApertureDefinition(ApertureDefinition { code: *code, aperture: match shape { ApertureType::Circle(diameter) => Aperture::Circle(Circle { diameter: *diameter, hole_diameter: None, }), ApertureType::Rect(x, y) => Aperture::Rectangle(Rectangular { x: *x, y: *y, hole_diameter: None, }), }, }) .into(), ); } commands } fn emit_poly>>(commands: &mut Vec, points: I) { let cf = CoordinateFormat::new(4, 6); let mut last: Option> = None; for point in points { if let Some(cmd) = match last { None => FunctionCode::DCode(DCode::Operation(Operation::Move(Coordinates::new( CoordinateNumber::try_from(point.x()).unwrap(), CoordinateNumber::try_from(point.y()).unwrap(), cf, )))) .into(), Some(last) => { let x = CoordinateNumber::try_from(point.x()).unwrap(); let y = CoordinateNumber::try_from(point.y()).unwrap(); let (dx, dy) = (point.x() - last.x(), point.y() - last.y()); match (dx < 1.0E-7 && dx > -1.0E-7, dy < 1.0E-7 && dy > -1.0E-7) { (true, true) => None, (_, true) => Some( // Y is the same, X has changed FunctionCode::DCode(DCode::Operation(Operation::Interpolate( Coordinates::at_x(x, cf), None, ))) .into(), ), (true, _) => Some( // X is the same, Y has changed FunctionCode::DCode(DCode::Operation(Operation::Interpolate( Coordinates::at_y(y, cf), None, ))) .into(), ), (_, _) => Some( FunctionCode::DCode(DCode::Operation(Operation::Interpolate( Coordinates::new(x, y, cf), None, ))) .into(), ), } } } { commands.push(gerber_types::Command::FunctionCode(cmd)); } last = Some(point.clone()); } } /// Serializes a representation of edge geometry in extender gerber format. pub fn serialize_edge(poly: Polygon) -> Result, ()> { let cf = CoordinateFormat::new(4, 6); let mut commands = gerber_prelude( cf, Some(FileFunction::Profile(Profile::NonPlated)), [(10, ApertureType::Circle(0.01))].iter(), ); commands.push(FunctionCode::DCode(DCode::SelectAperture(10)).into()); emit_poly(&mut commands, poly.exterior().points_iter()); for poly in poly.interiors() { emit_poly(&mut commands, poly.points_iter()); } commands.push(FunctionCode::MCode(MCode::EndOfFile).into()); Ok(commands) } #[derive(Debug, Copy, Clone)] struct FloatBits(f64); impl FloatBits { fn key(&self) -> u64 { unsafe { std::mem::transmute(self.0) } } } impl std::hash::Hash for FloatBits { fn hash(&self, state: &mut H) where H: std::hash::Hasher, { self.key().hash(state) } } impl PartialEq for FloatBits { fn eq(&self, other: &FloatBits) -> bool { self.key() == other.key() } } impl Eq for FloatBits {} /// Serializes a representation of copper/mask features in extender gerber format. pub fn serialize_layer( out_layer: super::Layer, features: Vec, bounds: geo::Rect, ) -> Result, ()> { let cf = CoordinateFormat::new(4, 6); // Collect all unique sizes to setup as apertures. let mut dias = HashMap::new(); let mut rects = HashMap::new(); for feature in &features { match feature { InnerAtom::Circle { radius, layer, .. } => { if out_layer == *layer { dias.insert(FloatBits(*radius * 2.0), ()); } } InnerAtom::Rect { rect, layer } => { if out_layer == *layer { rects.insert((FloatBits(rect.width()), FloatBits(rect.height())), ()); } } InnerAtom::Drill { .. } => (), // Drill hits are not on gerbers InnerAtom::VScoreH(_) | InnerAtom::VScoreV(_) => { if out_layer == super::Layer::FabricationInstructions { dias.insert(FloatBits(0.18), ()); } } } } // Assign codes to each aperture. let apertures: Vec<(i32, ApertureType)> = dias .keys() .map(|fb| ApertureType::Circle(fb.0)) .chain( rects .keys() .map(|(xfb, yfb)| ApertureType::Rect(xfb.0, yfb.0)), ) .enumerate() .map(|(i, f)| (10 + i as i32, f)) .collect(); let mut commands = gerber_prelude( cf, match out_layer { super::Layer::FrontCopper => Some(FileFunction::Copper { layer: 1, pos: ExtendedPosition::Top, copper_type: None, }), super::Layer::BackCopper => Some(FileFunction::Copper { layer: 2, pos: ExtendedPosition::Bottom, copper_type: None, }), super::Layer::FrontMask => Some(FileFunction::Soldermask { pos: Position::Top, index: None, }), super::Layer::BackMask => Some(FileFunction::Soldermask { pos: Position::Bottom, index: None, }), super::Layer::FrontLegend => Some(FileFunction::Legend { pos: Position::Top, index: None, }), super::Layer::BackLegend => Some(FileFunction::Legend { pos: Position::Bottom, index: None, }), super::Layer::FabricationInstructions => None, }, apertures.iter(), ); let mut last_aperture: Option = None; for feature in &features { match feature { InnerAtom::Circle { center, radius, layer, .. } => { if out_layer == *layer { let code = apertures.iter().find(|&(_, f)| matches!(f, ApertureType::Circle(f) if *f == (*radius * 2.0))).unwrap().0; if last_aperture != Some(code) { commands.push(FunctionCode::DCode(DCode::SelectAperture(code)).into()); last_aperture = Some(code); } let x = CoordinateNumber::try_from(center.x).unwrap(); let y = CoordinateNumber::try_from(center.y).unwrap(); commands.push(gerber_types::Command::FunctionCode( FunctionCode::DCode(DCode::Operation(Operation::Flash(Coordinates::new( x, y, cf, )))) .into(), )); } } InnerAtom::Rect { rect, layer } => { if out_layer == *layer { let code = apertures.iter().find(|&(_, f)| matches!(f, ApertureType::Rect(x, y) if *x == rect.width() && *y == rect.height())).unwrap().0; if last_aperture != Some(code) { commands.push(FunctionCode::DCode(DCode::SelectAperture(code)).into()); last_aperture = Some(code); } let x = CoordinateNumber::try_from(rect.center().x).unwrap(); let y = CoordinateNumber::try_from(rect.center().y).unwrap(); commands.push(gerber_types::Command::FunctionCode( FunctionCode::DCode(DCode::Operation(Operation::Flash(Coordinates::new( x, y, cf, )))) .into(), )); } } InnerAtom::Drill { .. } => (), // Drill hits are not on gerbers InnerAtom::VScoreH(y) => { if out_layer == super::Layer::FabricationInstructions { let code = apertures .iter() .find(|&(_, f)| matches!(f, ApertureType::Circle(f) if *f == 0.18)) .unwrap() .0; if last_aperture != Some(code) { commands.push(FunctionCode::DCode(DCode::SelectAperture(code)).into()); last_aperture = Some(code); } commands.push( FunctionCode::DCode(DCode::Operation(Operation::Move(Coordinates::new( CoordinateNumber::try_from(bounds.min().x - 3.).unwrap(), CoordinateNumber::try_from(*y).unwrap(), cf, )))) .into(), ); commands.push( FunctionCode::DCode(DCode::Operation(Operation::Interpolate( Coordinates::new( CoordinateNumber::try_from(bounds.max().x + 3.).unwrap(), CoordinateNumber::try_from(*y).unwrap(), cf, ), None, ))) .into(), ); flash_text( "V-SCORE", bounds.max().x + 0.5, *y + 1., code, cf, &mut commands, ); } } InnerAtom::VScoreV(x) => { if out_layer == super::Layer::FabricationInstructions { let code = apertures .iter() .find(|&(_, f)| matches!(f, ApertureType::Circle(f) if *f == 0.18)) .unwrap() .0; if last_aperture != Some(code) { commands.push(FunctionCode::DCode(DCode::SelectAperture(code)).into()); last_aperture = Some(code); } commands.push( FunctionCode::DCode(DCode::Operation(Operation::Move(Coordinates::new( CoordinateNumber::try_from(*x).unwrap(), CoordinateNumber::try_from(bounds.min().y - 3.).unwrap(), cf, )))) .into(), ); commands.push( FunctionCode::DCode(DCode::Operation(Operation::Interpolate( Coordinates::new( CoordinateNumber::try_from(*x).unwrap(), CoordinateNumber::try_from(bounds.max().y + 3.).unwrap(), cf, ), None, ))) .into(), ); } } } } commands.push(FunctionCode::MCode(MCode::EndOfFile).into()); Ok(commands) } #[cfg(feature = "text")] fn flash_text( text: &str, lx: f64, ly: f64, d_code: i32, cf: CoordinateFormat, commands: &mut Vec, ) { commands.push(FunctionCode::DCode(DCode::SelectAperture(d_code)).into()); // 6x8 font for y in 0..8 { for x in 0..6 * text.len() { let is_set = super::text::character_pixel(text.as_bytes()[x / 6] as char, (x % 6) as u32, y); if is_set { let x2 = CoordinateNumber::try_from(lx + (x as f64 * 0.11)).unwrap(); let y2 = CoordinateNumber::try_from(ly - (y as f64 * 0.11)).unwrap(); commands.push(gerber_types::Command::FunctionCode( FunctionCode::DCode(DCode::Operation(Operation::Flash(Coordinates::new( x2, y2, cf, )))) .into(), )); } } } } #[cfg(not(feature = "text"))] fn flash_text( text: &str, lx: f64, ly: f64, d_code: i32, cf: CoordinateFormat, commands: &mut Vec, ) { }