use ambassador::Delegate;
use derive_builder::Builder;
use crate::{
common::{Point2D, ScadObjectGeneric, Unit, D2},
internal::generate_sentence_repr,
scad_2d::{ScadPrimitive2D, ScadPrimitiveBody2D},
scad_display::{ambassador_impl_ScadDisplay, ScadDisplay},
value_type::RoundSize,
};
macro_rules! __impl_primitive_2d {
($prim_ty:ident) => {
__impl_builder_primitive!($prim_ty, D2);
__impl_primitive_to_code!($prim_ty, D2);
};
}
#[derive(Copy, Clone, Debug, PartialEq, Delegate)]
#[delegate(ScadDisplay)]
pub enum SquareSize {
N(Unit),
V(Point2D),
}
impl From<Unit> for SquareSize {
fn from(value: Unit) -> Self {
Self::N(value)
}
}
impl From<Point2D> for SquareSize {
fn from(value: Point2D) -> Self {
Self::V(value)
}
}
impl From<[Unit; 2]> for SquareSize {
fn from(value: [Unit; 2]) -> Self {
let [x, y] = value;
Self::V(Point2D::new(x, y))
}
}
#[derive(Builder, Copy, Clone, Debug, PartialEq)]
pub struct Square {
#[builder(setter(into))]
pub size: SquareSize,
#[builder(setter(into, strip_option), default)]
pub center: Option<bool>,
}
__impl_primitive_2d!(Square);
impl From<Square> for ScadObjectGeneric<D2> {
fn from(val: Square) -> Self {
let body = ScadPrimitiveBody2D::from(val);
let primitive = ScadPrimitive2D::new(body);
primitive.into()
}
}
impl ScadDisplay for Square {
fn repr_scad(&self) -> String {
generate_sentence_repr(
"square",
__generate_scad_options!(
("size", self.size); opt:(("center", self.center);)
),
)
}
}
#[derive(Builder, Copy, Clone, Debug, PartialEq)]
pub struct Circle {
#[builder(setter(custom))]
pub size: RoundSize,
#[builder(setter(into, strip_option), default)]
pub fa: Option<Unit>,
#[builder(setter(into, strip_option), default)]
pub r#fn: Option<u64>,
#[builder(setter(into, strip_option), default)]
pub fs: Option<Unit>,
}
__impl_primitive_2d!(Circle);
impl From<Circle> for ScadObjectGeneric<D2> {
fn from(val: Circle) -> Self {
let body = ScadPrimitiveBody2D::from(val);
let primitive = ScadPrimitive2D::new(body);
primitive.into()
}
}
impl CircleBuilder {
pub const fn r(&mut self, value: Unit) -> &mut Self {
let new = self;
new.size = Some(RoundSize::Radius(value));
new
}
pub const fn d(&mut self, value: Unit) -> &mut Self {
let new = self;
new.size = Some(RoundSize::Diameter(value));
new
}
}
impl ScadDisplay for Circle {
fn repr_scad(&self) -> String {
generate_sentence_repr(
"circle",
__generate_scad_options!(
(self.size.name(), self.size);
opt: (
("$fa", self.fa);
("$fn", self.r#fn);
("$fs", self.fs);
)
),
)
}
}
#[derive(Clone, Debug, PartialEq, derive_more::Deref)]
pub struct VecPoint2DEntry(pub Vec<Point2D>);
impl From<Vec<[Unit; 2]>> for VecPoint2DEntry {
fn from(value: Vec<[Unit; 2]>) -> Self {
Self(value.into_iter().map(|[x, y]| Point2D::new(x, y)).collect())
}
}
impl From<Vec<Point2D>> for VecPoint2DEntry {
fn from(value: Vec<Point2D>) -> Self {
Self(value)
}
}
impl From<VecPoint2DEntry> for Vec<Point2D> {
fn from(value: VecPoint2DEntry) -> Self {
value.0
}
}
#[derive(Builder, Clone, Debug, PartialEq)]
#[builder(build_fn(validate = "Self::validate"))]
pub struct Polygon {
#[builder(setter(custom))]
pub points: Vec<Point2D>,
#[builder(setter(into, strip_option), default)]
pub paths: Option<Vec<Vec<usize>>>,
#[builder(setter(into, strip_option), default)]
pub convexity: Option<u64>,
}
__impl_primitive_2d!(Polygon);
impl From<Polygon> for ScadObjectGeneric<D2> {
fn from(val: Polygon) -> Self {
let body = ScadPrimitiveBody2D::from(val);
let primitive = ScadPrimitive2D::new(body);
primitive.into()
}
}
impl PolygonBuilder {
fn validate(&self) -> Result<(), String> {
(|| -> Option<Result<(), String>> {
let pts: Vec<Point2D> = self.points.clone()?;
let pas: Vec<Vec<usize>> = self.paths.clone()??;
for (i, pa) in pas.into_iter().enumerate() {
for (j, vtx) in pa.into_iter().enumerate() {
if vtx >= pts.len() {
return Some(Err(format!("path index out of bounds: [{i}][{j}]:{vtx}")));
}
}
}
Some(Ok(()))
})()
.unwrap_or(Ok(()))
}
pub fn points<T: Into<VecPoint2DEntry>>(&mut self, value: T) -> &mut Self {
let new = self;
let entry: VecPoint2DEntry = value.into();
new.points = Some(entry.into());
new
}
}
impl ScadDisplay for Polygon {
fn repr_scad(&self) -> String {
generate_sentence_repr(
"polygon",
__generate_scad_options!(
("points", self.points.clone());
opt: (
("paths", self.paths.clone());
("convexity", self.convexity);
)
),
)
}
}
#[derive(Builder, Clone, Debug, PartialEq)]
pub struct Text {
#[builder(setter(into))]
pub text: String,
#[builder(setter(into, strip_option), default)]
pub size: Option<Unit>,
#[builder(setter(into, strip_option), default)]
pub font: Option<String>,
#[builder(setter(into, strip_option), default)]
pub halign: Option<String>,
#[builder(setter(into, strip_option), default)]
pub valign: Option<String>,
#[builder(setter(into, strip_option), default)]
pub spacing: Option<String>,
#[builder(setter(into, strip_option), default)]
pub direction: Option<String>,
#[builder(setter(into, strip_option), default)]
pub language: Option<String>,
#[builder(setter(into, strip_option), default)]
pub script: Option<String>,
#[builder(setter(into, strip_option), default)]
pub r#fn: Option<u64>,
}
__impl_primitive_2d!(Text);
impl From<Text> for ScadObjectGeneric<D2> {
fn from(val: Text) -> Self {
let body = ScadPrimitiveBody2D::from(val);
let primitive = ScadPrimitive2D::new(body);
primitive.into()
}
}
impl ScadDisplay for Text {
fn repr_scad(&self) -> String {
generate_sentence_repr(
"text",
__generate_scad_options!(
("", self.text.clone());
opt: (
("font", self.font.clone());
("size", self.size);
("halign", self.halign.clone());
("valign", self.valign.clone());
("spacing", self.spacing.clone());
("direction", self.direction.clone());
("language", self.language.clone());
("script", self.script.clone());
("$fn", self.r#fn);
)
),
)
}
}
#[derive(Builder, Clone, Debug, PartialEq)]
pub struct Import2D {
#[builder(setter(into))]
pub file: String,
#[builder(setter(into, strip_option), default)]
pub convexity: Option<u64>,
#[builder(setter(into, strip_option), default)]
pub id: Option<u64>,
#[builder(setter(into, strip_option), default)]
pub layer: Option<u64>,
#[builder(setter(into, strip_option), default)]
pub fa: Option<Unit>,
#[builder(setter(into, strip_option), default)]
pub r#fn: Option<u64>,
#[builder(setter(into, strip_option), default)]
pub fs: Option<Unit>,
}
__impl_primitive_2d!(Import2D);
impl From<Import2D> for ScadObjectGeneric<D2> {
fn from(val: Import2D) -> Self {
let body = ScadPrimitiveBody2D::from(val);
let primitive = ScadPrimitive2D::new(body);
primitive.into()
}
}
impl ScadDisplay for Import2D {
fn repr_scad(&self) -> String {
generate_sentence_repr(
"import",
__generate_scad_options!(
("", self.file.clone());
opt: (
("convexity", self.convexity);
("id", self.id);
("layer", self.layer);
("$fa", self.fa);
("$fn", self.r#fn);
("$fs", self.fs);
)
),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_square() {
assert_eq!(
SquareBuilder::default()
.size(3.)
.build()
.unwrap()
.repr_scad(),
"square(size = 3)"
);
assert_eq!(
SquareBuilder::default()
.size(3.)
.center(true)
.build()
.unwrap()
.repr_scad(),
"square(size = 3, center = true)"
);
assert_eq!(
SquareBuilder::default()
.size([3., 2.])
.build()
.unwrap()
.repr_scad(),
"square(size = [3, 2])"
);
assert_eq!(
SquareBuilder::default()
.size(Point2D::new(3., 2.))
.build()
.unwrap()
.repr_scad(),
"square(size = [3, 2])"
);
assert_eq!(
SquareBuilder::default()
.size([3., 2.])
.center(true)
.build()
.unwrap()
.repr_scad(),
"square(size = [3, 2], center = true)"
);
drop(SquareBuilder::default().center(true).build().unwrap_err());
}
#[test]
fn test_circle() {
assert_eq!(
CircleBuilder::default().r(3.).build().unwrap().repr_scad(),
"circle(r = 3)"
);
assert_eq!(
CircleBuilder::default().d(4.).build().unwrap().repr_scad(),
"circle(d = 4)"
);
assert_eq!(
CircleBuilder::default()
.r(3.)
.fa(0.5)
.r#fn(20_u64)
.build()
.unwrap()
.repr_scad(),
"circle(r = 3, $fa = 0.5, $fn = 20)"
);
assert_eq!(
CircleBuilder::default()
.r(3.)
.fs(40)
.fa(0.5)
.build()
.unwrap()
.repr_scad(),
"circle(r = 3, $fa = 0.5, $fs = 40)"
);
drop(
CircleBuilder::default()
.fa(0.5)
.r#fn(20_u64)
.fs(40.)
.build()
.unwrap_err(),
);
}
#[test]
fn test_polygon() {
let mut p0 = PolygonBuilder::default();
_ = p0.points(vec![
Point2D::new(1., 1.),
Point2D::new(-1., 2.),
Point2D::new(0., 0.),
]);
assert_eq!(
PolygonBuilder::default()
.points(vec![
Point2D::new(1., 1.),
Point2D::new(-1., 2.),
Point2D::new(0., 0.),
])
.build()
.unwrap()
.repr_scad(),
"polygon(points = [[1, 1], [-1, 2], [0, 0]])"
);
{
let mut tmp = p0.clone();
assert_eq!(
tmp.paths(vec![vec![0, 2, 1]]).build().unwrap().repr_scad(),
"polygon(points = [[1, 1], [-1, 2], [0, 0]], paths = [[0, 2, 1]])"
);
}
{
let mut tmp = p0.clone();
assert_eq!(
tmp.convexity(2_u64).build().unwrap().repr_scad(),
"polygon(points = [[1, 1], [-1, 2], [0, 0]], convexity = 2)"
);
}
let mut p1 = PolygonBuilder::default();
_ = p1.points(vec![
[2., 0.],
[1., 1.],
[-1., 1.],
[1., 0.],
[0.5, 0.5],
[-0.5, 0.5],
]);
{
let mut tmp = p1.clone();
assert_eq!(
tmp.paths([vec![0, 1, 2], vec![3, 4, 5]]).build().unwrap().repr_scad(),
"polygon(points = [[2, 0], [1, 1], [-1, 1], [1, 0], [0.5, 0.5], [-0.5, 0.5]], paths = [[0, 1, 2], [3, 4, 5]])"
);
}
assert_eq!(
p1.paths([vec![0, 1, 2], vec![6, 4, 5]])
.build()
.err()
.map(|e| e.to_string())
.unwrap_or_default(),
"path index out of bounds: [1][0]:6"
);
}
#[test]
fn test_text() {
assert_eq!(
TextBuilder::default()
.text("Hello World")
.build()
.unwrap()
.repr_scad(),
"text(\"Hello World\")"
);
assert_eq!(
TextBuilder::default()
.text("Hello World")
.font("LiberationSans-Regular")
.build()
.unwrap()
.repr_scad(),
"text(\"Hello World\", font = \"LiberationSans-Regular\")"
);
assert_eq!(
TextBuilder::default()
.text("Hello World")
.size(3.)
.build()
.unwrap()
.repr_scad(),
"text(\"Hello World\", size = 3)"
);
}
#[test]
fn test_import2d() {
assert_eq!(
Import2DBuilder::default()
.file("shape.svg")
.build()
.unwrap()
.repr_scad(),
"import(\"shape.svg\")"
);
assert_eq!(
Import2DBuilder::default()
.file("shape.svg")
.convexity(10_u64)
.build()
.unwrap()
.repr_scad(),
"import(\"shape.svg\", convexity = 10)"
);
}
}