solidrs 0.2.0

Rust library to generate openScad models. Inspired by SolidPython.
Documentation
use crate::{
    element::{Element, InnerElement},
    var::{Val, Var},
};
use std::{collections::HashMap, fmt::Write, path::Path};

pub trait Export: Sized {
    fn render_scad(self) -> String;

    fn save_as_scad(self, name: &str) {
        let is_scad = Path::new(name)
            .extension()
            .map_or(false, |ext| ext.eq_ignore_ascii_case("scad"));
        let name = if is_scad {
            String::from(name)
        } else {
            format!("{name}.scad")
        };
        let rendered = self.render_scad();
        std::fs::write(name, rendered).unwrap();
    }
}

impl Export for &Element<'_> {
    fn render_scad(self) -> String {
        let mut w = Writer::new();
        w.render_vars(&self.0);
        w.render(&self.0);
        w.str
    }
}

impl<'a, COLLECTION: IntoIterator<Item = &'a Element<'a>>> Export for COLLECTION {
    fn render_scad(self) -> String {
        let mut w = Writer::new();
        for element in self {
            w.render_vars(&element.0);
            w.render(&element.0);
        }
        w.str
    }
}

macro_rules! render {
    ($r:expr, $($arg:tt)*) => {
        {
            write!($r.str,"{}","    ".repeat($r.indent)).unwrap();
            write!($r.str,$($arg)*).unwrap();
        }
    };
}
macro_rules! renderln {
    ($r:expr, $($arg:tt)*) => {
        {
            write!($r.str,"{}","    ".repeat($r.indent)).unwrap();
            writeln!($r.str,$($arg)*).unwrap();
        }
    };
}
macro_rules! renderln_no_indent {
    ($r:expr, $($arg:tt)*) => {
        {
            writeln!($r.str,$($arg)*).unwrap();
        }
    };
}

struct Writer {
    str: String,
    indent: usize,
}

impl Writer {
    fn new() -> Self {
        Self {
            str: String::new(),
            indent: 0,
        }
    }

    fn render(&mut self, root: &InnerElement) {
        match root {
            InnerElement::Empty => {}
            InnerElement::Cube { x, y, z, centered } => self.render_cube(*x, *y, *z, *centered),
            InnerElement::Cylinder { h, r, centered } => self.render_cylinder(*r, *h, *centered),
            InnerElement::Square { x, y, centered } => self.render_square(*x, *y, *centered),
            InnerElement::Union { children } => self.render_union(children),
            InnerElement::Diff { children } => self.render_diff(children),
            InnerElement::Translate { x, y, z, child } => {
                self.render_transform("translate", *x, *y, *z, child);
            }
            InnerElement::Rotate { x, y, z, child } => {
                self.render_transform("rotate", *x, *y, *z, child);
            }
            InnerElement::RotateExtrude { angle, child } => self.render_rot_ext(*angle, child),
        }
    }

    fn render_cube(&mut self, x: Val, y: Val, z: Val, centered: bool) {
        renderln!(self, "cube([{x},{y},{z}]{});", center(centered));
    }

    fn render_transform(&mut self, transform: &str, x: Val, y: Val, z: Val, child: &InnerElement) {
        render!(self, "{transform}([{x},{y},{z}])");
        self.render_child(child);
    }

    fn render_diff(&mut self, children: &[InnerElement]) {
        render!(self, "difference()");
        self.render_children(children);
    }

    fn render_union(&mut self, children: &[InnerElement]) {
        render!(self, "union()");
        self.render_children(children);
    }

    fn render_children(&mut self, children: &[InnerElement]) {
        renderln_no_indent!(self, "{{");
        self.indent += 1;
        for child in children {
            self.render(child);
        }
        self.indent -= 1;
        renderln!(self, "}}");
    }

    fn render_child(&mut self, child: &InnerElement) {
        renderln_no_indent!(self, "{{");
        self.indent += 1;
        self.render(child);
        self.indent -= 1;
        renderln!(self, "}}");
    }

    fn render_square(&mut self, x: Val, y: Val, centered: bool) {
        renderln!(self, "square([{x},{y}]{});", center(centered));
    }

    fn render_rot_ext(&mut self, angle: Val, child: &InnerElement) {
        render!(self, "rotate_extrude(angle={angle})");
        self.render_child(child);
    }

    fn render_cylinder(&mut self, r: Val, h: Val, centered: bool) {
        renderln!(self, "cylinder({h}, r = {r}{});", center(centered));
    }

    fn render_vars(&mut self, element: &InnerElement) {
        let mut vars = HashMap::new();
        collect_vars(&mut vars, element);
        let mut vars = vars.values().collect::<Vec<_>>();
        vars.sort_unstable_by(|a, b| a.get_name().cmp(b.get_name()));
        for var in vars {
            if !var.get_comment().is_empty() {
                renderln!(self, "// {}", var.get_comment());
            }
            renderln!(self, "{} = {};", var.get_name(), var.get_val());
        }
    }
}

fn collect_vars<'a>(map: &mut HashMap<&str, &'a Var>, element: &'a InnerElement) {
    match element {
        InnerElement::Empty => {}
        InnerElement::Cube { x, y, z, .. } => add_vars(map, &[x, y, z]),
        InnerElement::Cylinder { h, r, .. } => add_vars(map, &[h, r]),
        InnerElement::Square { x, y, .. } => add_vars(map, &[x, y]),
        InnerElement::Union { children } | InnerElement::Diff { children } => {
            children.iter().for_each(|c| collect_vars(map, c));
        }
        InnerElement::Translate { x, y, z, child } | InnerElement::Rotate { x, y, z, child } => {
            add_vars(map, &[x, y, z]);
            collect_vars(map, child);
        }
        InnerElement::RotateExtrude { angle, child } => {
            add_vars(map, &[angle]);
            collect_vars(map, child);
        }
    }
}

fn add_vars<'a>(map: &mut HashMap<&str, &'a Var>, vars: &[&'a Val<'_>]) {
    for var in vars {
        if let Val::Var(var) = var {
            let name = var.get_name();
            if !name.is_empty() && !map.contains_key(name) {
                map.insert(name, *var);
            }
        }
    }
}

fn center(c: bool) -> &'static str {
    if c {
        ",center = true"
    } else {
        ""
    }
}