use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SheetTree {
pub thickness: f64,
pub root: Flat,
#[serde(default)]
pub default_inside_radius: f64,
#[serde(default = "half")]
pub default_k_factor: f64,
#[serde(default = "identity_placement")]
pub root_transform: [[f64; 3]; 4],
#[serde(default)]
pub name_suffixes: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Flat {
pub id: String,
pub outline: Vec<[f64; 2]>,
#[serde(default)]
pub edges: Vec<Edge>,
#[serde(default)]
pub holes: Vec<Hole>,
#[serde(default)]
pub hole_bends: Vec<HoleBend>,
#[serde(default)]
pub outline_curves: std::collections::BTreeMap<String, crate::NurbsCurve>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub end_cuts: Vec<PlaneCut>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PlaneCut {
pub point: [f64; 3],
pub normal: [f64; 3],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HoleBend {
pub id: String,
pub hole: usize,
pub segment: usize,
#[serde(default)]
pub reversed: bool,
pub angle_deg: f64,
pub inside_radius: f64,
#[serde(default = "half")]
pub k_factor: f64,
pub kind: HoleBendKind,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HoleBendKind {
Straight { child: Box<Flat> },
Collar { leg: f64 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hole {
pub outer: Vec<crate::NurbsCurve>,
#[serde(default)]
pub islands: Vec<Vec<crate::NurbsCurve>>,
#[serde(default)]
pub z_min: Option<f64>,
#[serde(default)]
pub z_max: Option<f64>,
}
impl Hole {
pub fn through(outer: Vec<crate::NurbsCurve>) -> Self {
Hole {
outer,
islands: Vec::new(),
z_min: None,
z_max: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Edge {
#[serde(default)]
pub id: String,
#[serde(default)]
pub bend: Option<Bend>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bend {
pub id: String,
pub angle_deg: f64,
pub inside_radius: f64,
#[serde(default = "half")]
pub k_factor: f64,
pub child: Box<Flat>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub reliefs: Vec<Hole>,
}
fn half() -> f64 {
0.5
}
pub fn identity_placement() -> [[f64; 3]; 4] {
[
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
]
}
impl Bend {
pub fn mid_radius(&self, thickness: f64) -> f64 {
(self.inside_radius + thickness * 0.5).max(1e-6)
}
pub fn neutral_radius(&self, thickness: f64) -> f64 {
self.mid_radius(thickness) + (self.k_factor - 0.5) * thickness
}
pub fn allowance(&self, thickness: f64) -> f64 {
self.angle_deg.to_radians().abs() * self.neutral_radius(thickness)
}
}
impl SheetTree {
pub(crate) fn mirrored(
&self,
point: crate::Vec3,
normal: crate::Vec3,
suffix: &str,
) -> Result<Option<SheetTree>, String> {
let Some(root) = self.root.mirrored(&|p| [-p[0], p[1]])? else {
return Ok(None);
};
let v = |a: [f64; 3]| crate::Vec3::new(a[0], a[1], a[2]);
let reflect = |a: crate::Vec3| a.sub(normal.scale(2.0 * a.dot(normal)));
let [o, x, y, z] = self.root_transform;
let o = v(o).sub(point);
let o = point.add(reflect(o));
let x = reflect(v(x)).scale(-1.0);
let (y, z) = (reflect(v(y)), reflect(v(z)));
let mut name_suffixes = self.name_suffixes.clone();
name_suffixes.push(suffix.to_string());
Ok(Some(SheetTree {
root,
root_transform: [[o.x, o.y, o.z], [x.x, x.y, x.z], [y.x, y.y, y.z], [z.x, z.y, z.z]],
name_suffixes,
..self.clone()
}))
}
}
impl SheetTree {
pub(crate) fn mirror_joined(
&self,
point: crate::Vec3,
normal: crate::Vec3,
tag: &str,
) -> Result<Option<SheetTree>, String> {
let v = |a: [f64; 3]| crate::Vec3::new(a[0], a[1], a[2]);
let [o, x, y, z] = self.root_transform;
let (o, x, y, z) = (v(o), v(x), v(y), v(z));
if normal.dot(z).abs() > 1e-9 {
return Ok(None);
}
let root = &self.root;
let n = root.outline.len();
let world = |p: [f64; 2]| o.add(x.scale(p[0])).add(y.scale(p[1]));
let on_plane = |p: [f64; 2]| world(p).sub(point).dot(normal).abs() <= 1e-7;
let Some(k) = (0..n).find(|&k| {
root.edges.get(k).is_some_and(|edge| edge.bend.is_none() && !root.outline_curves.contains_key(&edge.id))
&& on_plane(root.outline[k])
&& on_plane(root.outline[(k + 1) % n])
}) else {
return Ok(None);
};
let (a, b) = (root.outline[k], root.outline[(k + 1) % n]);
let length = (b[0] - a[0]).hypot(b[1] - a[1]);
let t = [(b[0] - a[0]) / length, (b[1] - a[1]) / length];
if root.outline.iter().any(|p| t[0] * (p[1] - a[1]) - t[1] * (p[0] - a[0]) < -1e-7) {
return Err("the mirror plane passes through the part; pick a plate side wall at its edge".into());
}
let reflect = |p: [f64; 2]| {
let d = [p[0] - a[0], p[1] - a[1]];
let along = d[0] * t[0] + d[1] * t[1];
[a[0] + 2.0 * along * t[0] - d[0], a[1] + 2.0 * along * t[1] - d[1]]
};
let Some(mut twin) = root.mirrored(&reflect)? else {
return Err("a part with flanges on hole rims cannot be mirrored yet".into());
};
for edge in &mut twin.edges {
if let Some(curve) = twin.outline_curves.remove(&edge.id) {
twin.outline_curves.insert(format!("{}@{tag}", edge.id), curve);
}
edge.id = format!("{}@{tag}", edge.id);
if let Some(bend) = &mut edge.bend {
bend.retag(tag);
}
}
let jk = (2 * n - 2 - k) % n;
let mut outline = Vec::with_capacity(2 * n - 2);
let mut edges = Vec::with_capacity(2 * n - 2);
for i in 0..n {
outline.push(root.outline[(k + 1 + i) % n]);
if i + 1 < n {
edges.push(root.edges[(k + 1 + i) % n].clone());
}
}
edges.push(twin.edges[(jk + 1) % n].clone());
for i in 2..n {
outline.push(twin.outline[(jk + i) % n]);
edges.push(twin.edges[(jk + i) % n].clone());
}
let mut merged = Flat {
outline,
edges,
holes: root.holes.iter().chain(&twin.holes).cloned().collect(),
outline_curves: root.outline_curves.clone().into_iter().chain(twin.outline_curves).collect(),
..root.clone()
};
merge_in_line(&mut merged, n - 1, false);
merge_in_line(&mut merged, 0, true);
Ok(Some(SheetTree { root: merged, ..self.clone() }))
}
}
fn merge_in_line(flat: &mut Flat, vertex: usize, twin_in: bool) {
let n = flat.outline.len();
let (prev, next) = ((vertex + n - 1) % n, (vertex + 1) % n);
let (p, v, q) = (flat.outline[prev], flat.outline[vertex], flat.outline[next]);
let (d1, d2) = ([v[0] - p[0], v[1] - p[1]], [q[0] - v[0], q[1] - v[1]]);
let (l1, l2) = (d1[0].hypot(d1[1]), d2[0].hypot(d2[1]));
if (d1[0] * d2[1] - d1[1] * d2[0]).abs() > 1e-9 * l1 * l2 || d1[0] * d2[0] + d1[1] * d2[1] <= 0.0 {
return;
}
let (incoming, outgoing) = (&flat.edges[prev], &flat.edges[vertex]);
if flat.outline_curves.contains_key(&incoming.id) || flat.outline_curves.contains_key(&outgoing.id) {
return;
}
let plain_wall = |bend: &Bend, span: f64| {
let c = &bend.child;
let leg = c.outline.get(1).map_or(0.0, |p| p[0]);
c.outline.len() == 4
&& c.holes.is_empty()
&& c.hole_bends.is_empty()
&& c.outline_curves.is_empty()
&& c.edges.iter().all(|e| e.bend.is_none())
&& [[0.0, 0.0], [leg, 0.0], [leg, span], [0.0, span]]
.iter()
.zip(&c.outline)
.all(|(e, a)| (e[0] - a[0]).abs() <= 1e-9 && (e[1] - a[1]).abs() <= 1e-9)
};
let keep = if twin_in { outgoing } else { incoming };
let edge = match (&incoming.bend, &outgoing.bend) {
(None, None) => keep.clone(),
(Some(b1), Some(b2))
if b1.angle_deg == b2.angle_deg
&& b1.inside_radius == b2.inside_radius
&& b1.k_factor == b2.k_factor
&& plain_wall(b1, l1)
&& plain_wall(b2, l2)
&& (b1.child.outline[1][0] - b2.child.outline[1][0]).abs() <= 1e-9 =>
{
let mut edge = keep.clone();
let child = &mut edge.bend.as_mut().expect("bent edge").child;
let span = l1 + l2;
child.outline[2][1] = span;
child.outline[3][1] = span;
edge
}
_ => return,
};
flat.outline.remove(vertex);
flat.edges[prev] = edge;
flat.edges.remove(vertex);
}
impl Bend {
fn retag(&mut self, tag: &str) {
self.id = format!("{}@{tag}", self.id);
self.child.retag(tag);
}
}
impl Flat {
fn retag(&mut self, tag: &str) {
self.id = format!("{}@{tag}", self.id);
self.outline_curves = std::mem::take(&mut self.outline_curves)
.into_iter()
.map(|(id, curve)| (format!("{id}@{tag}"), curve))
.collect();
for edge in &mut self.edges {
edge.id = format!("{}@{tag}", edge.id);
if let Some(bend) = &mut edge.bend {
bend.retag(tag);
}
}
}
}
impl Flat {
fn mirrored(&self, map: &dyn Fn([f64; 2]) -> [f64; 2]) -> Result<Option<Flat>, String> {
let n = self.outline.len();
if !self.hole_bends.is_empty() || self.edges.len() != n {
return Ok(None);
}
let curve = |c: &crate::NurbsCurve| -> Result<crate::NurbsCurve, String> {
let points = c
.control_points
.iter()
.map(|p| {
let q = p.point()?;
let [x, y] = map([q.x, q.y]);
Ok(crate::Vec4::from_point(crate::Vec3::new(x, y, q.z), p.w))
})
.collect::<Result<Vec<_>, String>>()?;
crate::NurbsCurve::new(c.degree, c.knots.clone(), points)?.reversed()
};
let chain = |curves: &[crate::NurbsCurve]| -> Result<Vec<crate::NurbsCurve>, String> {
curves.iter().rev().map(curve).collect()
};
let mut edges = Vec::with_capacity(n);
for j in 0..n {
let i = (2 * n - 2 - j) % n;
let edge = &self.edges[i];
let bend = match &edge.bend {
None => None,
Some(bend) => {
let (a, b) = (self.outline[i], self.outline[(i + 1) % n]);
let length = (b[0] - a[0]).hypot(b[1] - a[1]);
let Some(child) = bend.child.mirrored(&|p| [p[0], length - p[1]])? else {
return Ok(None);
};
Some(Bend { child: Box::new(child), ..bend.clone() })
}
};
edges.push(Edge { id: edge.id.clone(), bend });
}
let mut holes = Vec::with_capacity(self.holes.len());
for hole in &self.holes {
holes.push(Hole {
outer: chain(&hole.outer)?,
islands: hole.islands.iter().map(|island| chain(island)).collect::<Result<_, _>>()?,
..hole.clone()
});
}
let mut outline_curves = std::collections::BTreeMap::new();
for (id, c) in &self.outline_curves {
outline_curves.insert(id.clone(), curve(c)?);
}
Ok(Some(Flat {
id: self.id.clone(),
outline: (0..n).map(|j| map(self.outline[(2 * n - 1 - j) % n])).collect(),
edges,
holes,
hole_bends: Vec::new(),
outline_curves,
end_cuts: self
.end_cuts
.iter()
.map(|cut| {
let [x, y, z] = cut.point;
let [nx, ny, nz] = cut.normal;
let (o, p, q) = (map([0.0, 0.0]), map([x, y]), map([nx, ny]));
PlaneCut { point: [p[0], p[1], z], normal: [q[0] - o[0], q[1] - o[1], nz] }
})
.collect(),
}))
}
pub(crate) fn find_by_id(&self, flat_id: &str) -> Option<&Self> {
if self.id == flat_id {
return Some(self);
}
for edge in &self.edges {
if let Some(bend) = &edge.bend {
if let Some(found) = bend.child.find_by_id(flat_id) {
return Some(found);
}
}
}
for hole_bend in &self.hole_bends {
if let HoleBendKind::Straight { child } = &hole_bend.kind {
if let Some(found) = child.find_by_id(flat_id) {
return Some(found);
}
}
}
None
}
pub(crate) fn find_by_id_mut(&mut self, flat_id: &str) -> Option<&mut Self> {
if self.id == flat_id {
return Some(self);
}
for edge in &mut self.edges {
if let Some(bend) = &mut edge.bend {
if let Some(found) = bend.child.find_by_id_mut(flat_id) {
return Some(found);
}
}
}
for hole_bend in &mut self.hole_bends {
if let HoleBendKind::Straight { child } = &mut hole_bend.kind {
if let Some(found) = child.find_by_id_mut(flat_id) {
return Some(found);
}
}
}
None
}
pub(super) fn densified_outline(&self) -> Result<Vec<[f64; 2]>, String> {
if self.outline_curves.is_empty() {
return Ok(self.outline.clone());
}
let mut poly = Vec::with_capacity(self.outline.len());
for (i, vertex) in self.outline.iter().enumerate() {
match self
.edges
.get(i)
.and_then(|edge| self.outline_curves.get(&edge.id))
{
Some(curve) => {
let [t0, t1] = curve.domain()?;
for step in 0..16 {
let t = t0 + (t1 - t0) * (step as f64) / 16.0;
let p = curve.evaluate(t)?;
poly.push([p.x, p.y]);
}
}
None => poly.push(*vertex),
}
}
Ok(poly)
}
}