maker-panel 0.13.0

Generator for modular mechanical panels
Documentation
use maker_panel::{Layer, Panel};
use std::path::PathBuf;
use structopt::StructOpt;

#[derive(Debug)]
enum Err {
    IO(std::io::Error),
    General(maker_panel::Err),
    Zip(zip::result::ZipError),
    SpecError(usize, String, maker_panel::SpecErr),
}

/// Represents an output format provided for the gen command.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Fmt {
    Edge,
    FrontCopper,
    FrontMask,
    FrontLegend,
    BackCopper,
    BackMask,
    BackLegend,
    PlatedDrill,
    NonPlatedDrill,
    Zip,
    #[cfg(feature = "tessellate")]
    Stl,
    FabInstructions,
}

impl Fmt {
    fn all_formats() -> &'static [Fmt] {
        &[
            Fmt::Edge,
            Fmt::FrontCopper,
            Fmt::FrontMask,
            Fmt::FrontLegend,
            Fmt::BackCopper,
            Fmt::BackMask,
            Fmt::BackLegend,
            Fmt::PlatedDrill,
            Fmt::NonPlatedDrill,
            // Fmt::Stl, - exclude from list so its not generated by default
            // Fmt::FabInstructions - exclude from list as special-case
        ]
    }

    fn file_suffix(&self) -> &'static str {
        match self {
            Fmt::Edge => "Edge.Cuts.gm1",
            Fmt::FrontCopper => "F.Cu.gtl",
            Fmt::FrontMask => "F.Mask.gts",
            Fmt::FrontLegend => "F.SilkS.gto",
            Fmt::BackCopper => "B.Cu.gbl",
            Fmt::BackMask => "B.Mask.gbs",
            Fmt::BackLegend => "B.SilkS.gto",
            Fmt::PlatedDrill => "PTH.drl",
            Fmt::NonPlatedDrill => "NPTH.drl",
            Fmt::Zip => "gerbers.zip",
            #[cfg(feature = "tessellate")]
            Fmt::Stl => "extrusion.stl",
            Fmt::FabInstructions => "Cmts.User",
        }
    }

    fn serialize_to(&self, panel: &Panel, w: &mut impl std::io::Write) -> Result<(), Err> {
        match self {
            Fmt::Edge => panel.serialize_gerber_edges(w).map_err(|e| Err::General(e)),
            Fmt::FrontCopper => panel
                .serialize_gerber_layer(Layer::FrontCopper, w)
                .map_err(|e| Err::General(e)),
            Fmt::FrontMask => panel
                .serialize_gerber_layer(Layer::FrontMask, w)
                .map_err(|e| Err::General(e)),
            Fmt::FrontLegend => panel
                .serialize_gerber_layer(Layer::FrontLegend, w)
                .map_err(|e| Err::General(e)),
            Fmt::BackCopper => panel
                .serialize_gerber_layer(Layer::BackCopper, w)
                .map_err(|e| Err::General(e)),
            Fmt::BackMask => panel
                .serialize_gerber_layer(Layer::BackMask, w)
                .map_err(|e| Err::General(e)),
            Fmt::BackLegend => panel
                .serialize_gerber_layer(Layer::BackLegend, w)
                .map_err(|e| Err::General(e)),
            Fmt::FabInstructions => panel
                .serialize_gerber_layer(Layer::FabricationInstructions, w)
                .map_err(|e| Err::General(e)),
            Fmt::PlatedDrill => panel.serialize_drill(w, true).map_err(|e| Err::IO(e)),
            Fmt::NonPlatedDrill => panel.serialize_drill(w, false).map_err(|e| Err::IO(e)),
            Fmt::Zip => {
                let mut cursor = std::io::Cursor::new(Vec::with_capacity(4 * 1024));
                let mut zip = zip::ZipWriter::new(&mut cursor);
                let options = zip::write::FileOptions::default()
                    .compression_method(zip::CompressionMethod::Stored)
                    .unix_permissions(0o755);

                for fmt in Fmt::all_formats() {
                    zip.start_file(fmt.file_suffix(), options)
                        .map_err(|e| Err::Zip(e))?;
                    fmt.serialize_to(panel, &mut zip)?;
                }
                if panel.has_fab_markings() {
                    zip.start_file(Fmt::FabInstructions.file_suffix(), options)
                        .map_err(|e| Err::Zip(e))?;
                    Fmt::FabInstructions.serialize_to(panel, &mut zip)?;

                    zip.start_file("fab-notes.txt", options)
                        .map_err(|e| Err::Zip(e))?;
                    use std::io::Write;
                    zip.write(b"V-SCORE: See Cmts.User gerber file.\n")
                        .map_err(|e| Err::IO(e))?;
                }
                zip.finish().map_err(|e| Err::Zip(e))?;

                drop(zip);
                w.write(&cursor.into_inner()).map_err(|e| Err::IO(e))?;
                Ok(())
            }
            #[cfg(feature = "tessellate")]
            Fmt::Stl => {
                let (verts, inds) = panel.tessellate_3d().map_err(|e| Err::General(e))?;
                use stl_io::{Normal, Triangle, Vertex};

                let v_conv = |idx: u16| {
                    let v = verts[idx as usize];
                    Vertex::new([v[0] as f32, v[1] as f32, v[2] as f32])
                };

                let mesh = inds
                    .chunks_exact(3)
                    .map(|inds| {
                        let verts = [v_conv(inds[0]), v_conv(inds[1]), v_conv(inds[2])];

                        // Compute the normal of the face via dot product of the verts.
                        // We don't use a real math library because im still learning
                        // this stuff, and i wanted to do it by hand (feel free to PR).
                        let u = [
                            verts[1][0] - verts[0][0],
                            verts[1][1] - verts[0][1],
                            verts[1][2] - verts[0][2],
                        ];
                        let v = [
                            verts[2][0] - verts[0][0],
                            verts[2][1] - verts[0][1],
                            verts[2][2] - verts[0][2],
                        ];
                        let normal = [
                            (u[1] * v[2]) - (u[2] * v[1]),
                            (u[2] * v[0]) - (u[0] * v[2]),
                            (u[0] * v[1]) - (u[1] * v[0]),
                        ];

                        Triangle {
                            normal: Normal::new(normal),
                            vertices: verts,
                        }
                    })
                    .collect::<Vec<_>>();

                // println!("indices: {:?}", inds.chunks_exact(3).collect::<Vec<_>>());
                stl_io::write_stl(w, mesh.iter()).unwrap();
                Ok(())
            }
        }
    }
}

impl std::str::FromStr for Fmt {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "edge" => Ok(Fmt::Edge),
            "f.cu" => Ok(Fmt::FrontCopper),
            "f.mask" => Ok(Fmt::FrontMask),
            "f.legend" => Ok(Fmt::FrontLegend),
            "b.cu" => Ok(Fmt::BackCopper),
            "b.mask" => Ok(Fmt::BackMask),
            "b.legend" => Ok(Fmt::BackLegend),
            "drl" | "pdrl" => Ok(Fmt::PlatedDrill),
            "ndrl" | "npdrl" => Ok(Fmt::NonPlatedDrill),
            "zip" | "all" => Ok(Fmt::Zip),
            #[cfg(feature = "tessellate")]
            "stl" => Ok(Fmt::Stl),
            "fab" | "cmts.user" => Ok(Fmt::FabInstructions),
            _ => Err(format!("no such fmt: {}", s).to_string()),
        }
    }
}

/// Represents the --size parameter from the command line.
#[derive(Debug, PartialEq, Clone)]
pub struct RenderFitTo(usvg::FitTo);

impl std::str::FromStr for RenderFitTo {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s == "mm" {
            return Ok(RenderFitTo(usvg::FitTo::Original));
        };

        if s.starts_with("z:") {
            return Ok(RenderFitTo(usvg::FitTo::Zoom(
                s[2..]
                    .parse::<f32>()
                    .map_err(|e| format!("invalid zoom: {}", e))?,
            )));
        };

        Ok(RenderFitTo(usvg::FitTo::Width(
            s.parse::<u32>()
                .map_err(|e| format!("invalid width: {}", e))?,
        )))
    }
}

#[derive(StructOpt, Debug, PartialEq, Clone)]
pub enum Cmd {
    #[structopt(name = "png", about = "Renders a PNG visualizing the panel.")]
    Render {
        #[structopt(
            name = "size",
            short = "s",
            long = "size",
            about = "Specify z:<zoom> or width in pixels",
            default_value = "z:21.0"
        )]
        fit_to: RenderFitTo,

        #[structopt(
            name = "grid-separation",
            short = "g",
            long = "grid-separation",
            about = "The spacing between lines on the grid, if set"
        )]
        grid_sep: Option<isize>,

        output: PathBuf,
    },
    #[structopt(name = "gen", about = "Generates CAD files.")]
    Gen {
        #[structopt(
            name = "fmt",
            short = "f",
            long = "fmt",
            about = "Specifies what output format to generate",
            default_value = "zip"
        )]
        fmt: Fmt,

        #[structopt(
            name = "output",
            short = "o",
            long = "output",
            about = "File path where the generated output should be written"
        )]
        output: Option<PathBuf>,
    },
}

#[derive(Debug, StructOpt, Clone)]
#[structopt(
    name = "maker-panel",
    about = "Generates mechanical PCBs based on repeating geometry"
)]
struct Opt {
    #[structopt(
        name = "from-files",
        short = "f",
        long,
        about = "Whether the inputs should be interpreted as file paths to read, or as literal input"
    )]
    from_files: bool,

    #[structopt(
        name = "convex-hull",
        short = "ch",
        long = "hull",
        about = "Whether to apply a convex hull transform on the final exterior geometry"
    )]
    convex_hull: bool,

    input_spec: Vec<String>,

    #[structopt(subcommand)]
    cmd: Cmd,
}

impl Opt {
    fn panel(&self, panel: &mut Panel) -> Result<(), Err> {
        panel.convex_hull(self.convex_hull);

        for (i, s) in self.input_spec.iter().enumerate() {
            let content = if self.from_files {
                use std::fs::read;
                String::from_utf8_lossy(&read(s).map_err(|e| Err::IO(e))?).to_string()
            } else {
                s.to_string()
            };
            panel
                .push_spec(&content)
                .map_err(|e| Err::SpecError(i, s.clone(), e))?;
        }
        Ok(())
    }
}

fn main() {
    let args = Opt::from_args();

    let mut panel = Panel::new();
    match args.panel(&mut panel) {
        Ok(_) => {}
        Err(e) => {
            if let Err::SpecError(idx, fname, maker_panel::SpecErr::Parse(msg)) = e {
                eprintln!("Input spec {} ({}) had parsing errors:", idx + 1, fname);
                eprint!("{}", msg);
            } else {
                eprintln!("Error: {:?}", e);
            }
            std::process::exit(1);
        }
    };
    // panel.convex_hull(true);
    // panel.push(Rect::with_center([0.0, -2.5].into(), 5., 5.));
    // panel.push_spec(DEMO_SPEC).unwrap();

    if let Err(e) = run_cmd(args, panel) {
        eprintln!("Error: {:?}", e);
        std::process::exit(1);
    };
}

fn run_cmd(args: Opt, mut panel: Panel) -> Result<(), Err> {
    let mut stdout = std::io::stdout();

    match args.cmd {
        Cmd::Render {
            output,
            fit_to,
            grid_sep,
        } => {
            panel.set_grid_separation(grid_sep);
            let n = panel.make_svg().unwrap();
            // println!("{}", n.to_string(usvg::XmlOptions::default()));
            resvg::render_node(&n.root(), fit_to.0, Some(usvg::Color::white()))
                .unwrap()
                .save_png(output)
                .unwrap();
            Ok(())
        }
        Cmd::Gen { fmt, output: None } => fmt.serialize_to(&panel, &mut stdout),
        Cmd::Gen {
            fmt,
            output: Some(p),
        } => {
            let mut file = std::fs::File::create(&p).map_err(|e| Err::IO(e))?;
            fmt.serialize_to(&panel, &mut file)
        }
    }
}