solidrs 0.4.0

Rust library to generate openScad models. Inspired by SolidPython.
Documentation
use crate::{
    calc::CalcOp,
    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>> 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),
            InnerElement::Fa { fa, child } => self.render_config_param("fa", fa, child),
            InnerElement::Fs { fs, child } => self.render_config_param("fs", fs, child),
            InnerElement::Fn { f_n, child } => self.render_config_param("fn", f_n, 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_config_param(&mut self, name: &str, val: &Val, child: &InnerElement) {
        renderln!(self, "${name} = {val};");
        self.render(child);
    }

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

fn collect_vars(map: &mut HashMap<&str, Var>, element: &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: val, child }
        | InnerElement::Fs { fs: val, child }
        | InnerElement::Fn { f_n: val, child }
        | InnerElement::Fa { fa: val, child } => {
            add_vars(map, &[val]);
            collect_vars(map, child);
        }
    }
}

fn add_vars(map: &mut HashMap<&str, Var>, vars: &[&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);
            }
        }
        if let Val::Calc(calc) = var {
            match calc.op {
                CalcOp::Neg => add_vars(map, &[&calc.a]),
                CalcOp::Add | CalcOp::Sub | CalcOp::Mul | CalcOp::Div => {
                    add_vars(map, &[&calc.a, &calc.b]);
                }
            }
        }
    }
}

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