Skip to main content

geop_ops_sheetmetal/
thicken.rs

1//! Thickening one side of a sheet into the solid sheet of metal it is:
2//! [`SheetSurface`], and [`thicken`], which builds its solid in one go.
3//!
4//! A sheet-metal part is a surface — its *A side* — made solid by giving it
5//! a thickness: every face of the A side has its copy on the *B side*, a
6//! thickness further along the surface's normal, and every edge on the A
7//! side's boundary sweeps a *wall* across the thickness to its copy. The
8//! faces here are planes and cylinders around an axis, whose copies are
9//! exact: a plane moved along its normal, a cylinder of a radius one
10//! thickness larger or smaller about the same axis. So the B side is given
11//! with the A side, and nothing is approximated.
12
13use std::collections::BTreeMap;
14
15use geop_core_geometry::{
16    nurb_curve::{NurbCurve2D, NurbCurve3D},
17    nurb_surface::NurbSurface3D,
18};
19use geop_core_math::{
20    geop_error::{GeopError, GeopResult, WithContext},
21    scalars::Scalar,
22    vector::{Vector2, Vector3},
23    with_context,
24};
25use geop_core_topology::{
26    Curve3, Sense,
27    build::{BodySpec, BuiltBody, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
28};
29use geop_ops_model::{BodyNames, NamedModel, Namer};
30
31/// A vertex of the A side, and where it is on the B side.
32#[derive(Clone, Debug)]
33pub struct SheetVertex {
34    pub a: Vector3,
35    pub b: Vector3,
36    /// Its vertices are `N(a)` and `N(b)`, the edge across the thickness
37    /// between them, on the boundary, `N`.
38    pub name: Namer,
39}
40
41/// An edge of the A side, from vertex `start` to vertex `end`, and its copy
42/// on the B side: the same degree, knots and weights, so that the wall
43/// between them is ruled exactly.
44#[derive(Clone, Debug)]
45pub struct SheetEdge {
46    pub a: Curve3,
47    pub b: Curve3,
48    pub start: usize,
49    pub end: usize,
50    /// Its edges are `N(a)` and `N(b)`, its wall, on the boundary, `N`.
51    pub name: Namer,
52}
53
54/// One coedge of a face's loop: an edge in a sense, and its pcurve — on
55/// both of the face's surfaces, which share their parametrization.
56#[derive(Clone, Debug)]
57pub struct SheetCoedge {
58    pub edge: usize,
59    pub sense: Sense,
60    pub pcurve: NurbCurve2D,
61}
62
63/// A face of the A side, and its copy on the B side, parametrized alike:
64/// each with `∂u × ∂v` along the sheet's normal, from the A side towards
65/// the B side, and its loops running counter-clockwise in `(u, v)` — the
66/// outer one, holes clockwise.
67#[derive(Clone, Debug)]
68pub struct SheetFace {
69    pub a: NurbSurface3D,
70    pub b: NurbSurface3D,
71    pub outer: Vec<SheetCoedge>,
72    pub holes: Vec<Vec<SheetCoedge>>,
73    /// Its faces are `N(a)` and `N(b)`.
74    pub name: Namer,
75}
76
77/// The A side of a sheet, with its B side: what [`thicken`] builds a solid
78/// from.
79#[derive(Clone, Debug, Default)]
80pub struct SheetSurface {
81    pub vertices: Vec<SheetVertex>,
82    pub edges: Vec<SheetEdge>,
83    pub faces: Vec<SheetFace>,
84}
85
86impl SheetSurface {
87    /// Per edge, the faces using it: `(face, sense)`.
88    fn uses(&self) -> Vec<Vec<(usize, Sense)>> {
89        let mut uses = vec![Vec::new(); self.edges.len()];
90        for (f, face) in self.faces.iter().enumerate() {
91            for coedge in std::iter::once(&face.outer).chain(&face.holes).flatten() {
92                uses[coedge.edge].push((f, coedge.sense));
93            }
94        }
95        uses
96    }
97
98    /// Per edge, whether it lies on the boundary — used by one face only —
99    /// checked to be used once or twice, then in opposite senses.
100    fn boundary(&self) -> GeopResult<Vec<bool>> {
101        self.uses()
102            .iter()
103            .enumerate()
104            .map(|(e, uses)| match uses[..] {
105                [_] => Ok(true),
106                [(_, a), (_, b)] if a != b => Ok(false),
107                _ => Err(GeopError::new(format!(
108                    "the sheet's edge {} is used {:?} by its faces",
109                    self.edges[e].name.root(),
110                    uses
111                ))),
112            })
113            .collect()
114    }
115
116    /// The names of the faces [`thicken`] builds, before `rename`: both
117    /// sides of every face, and a wall on every edge of the boundary.
118    pub fn face_names(&self) -> GeopResult<Vec<String>> {
119        let boundary = self.boundary()?;
120        let mut names: Vec<String> = self
121            .faces
122            .iter()
123            .flat_map(|f| [f.name.name(&["a"]), f.name.name(&["b"])])
124            .collect();
125        names.extend(
126            self.edges
127                .iter()
128                .zip(&boundary)
129                .filter(|(_, b)| **b)
130                .map(|(e, _)| e.name.root()),
131        );
132        Ok(names)
133    }
134}
135
136/// Builds the solid sheet of metal `sheet` is the A side of — both sides of
137/// each of its faces, a wall on each edge of its boundary — named `solid`,
138/// and each of its entities as its [`SheetSurface`] entry says, through
139/// `rename`.
140///
141/// Every vertex on the boundary must have one boundary edge arriving and
142/// one leaving, so that the walls close up around it: the sheet is a
143/// surface with a boundary, not two touching at a point.
144pub fn thicken(
145    model: &mut NamedModel,
146    sheet: &SheetSurface,
147    solid: &str,
148    rename: &dyn Fn(String) -> String,
149) -> GeopResult<BuiltBody> {
150    let ctx = with_context!(
151        "thicken({solid}: {} faces, {} edges, {} vertices)",
152        sheet.faces.len(),
153        sheet.edges.len(),
154        sheet.vertices.len()
155    );
156    let boundary = sheet.boundary().with_context(ctx)?;
157    let nv = sheet.vertices.len();
158    let mut spec = BodySpec::new(true);
159    let mut names = BodyNames {
160        solid: Some(solid.to_string()),
161        ..BodyNames::default()
162    };
163    // Vertex `v` is `v` on the A side and `nv + v` on the B side.
164    for side in ["a", "b"] {
165        for v in &sheet.vertices {
166            spec.vertices.push(if side == "a" { v.a } else { v.b });
167            names.vertices.push(rename(v.name.name(&[side])));
168        }
169    }
170    // Edge `e` is `2e` on the A side and `2e + 1` on the B side.
171    for e in &sheet.edges {
172        spec.edges.push(EdgeSpec {
173            curve: e.a.clone(),
174            start: e.start,
175            end: e.end,
176        });
177        names.edges.push(rename(e.name.name(&["a"])));
178        spec.edges.push(EdgeSpec {
179            curve: e.b.clone(),
180            start: nv + e.start,
181            end: nv + e.end,
182        });
183        names.edges.push(rename(e.name.name(&["b"])));
184    }
185    // An edge across the thickness at every vertex of the boundary, which
186    // must have one boundary edge arriving and one leaving.
187    let mut arriving = vec![0; nv];
188    let mut leaving = vec![0; nv];
189    for (e, edge) in sheet.edges.iter().enumerate() {
190        if boundary[e] {
191            leaving[edge.start] += 1;
192            arriving[edge.end] += 1;
193        }
194    }
195    let mut across: BTreeMap<usize, usize> = BTreeMap::new();
196    for (v, vertex) in sheet.vertices.iter().enumerate() {
197        match (arriving[v] + leaving[v], arriving[v] == leaving[v]) {
198            (0, _) => {}
199            (2, _) => {
200                spec.edges.push(EdgeSpec {
201                    curve: NurbCurve3D::line(vertex.a, vertex.b).with_context(ctx)?,
202                    start: v,
203                    end: nv + v,
204                });
205                names.edges.push(rename(vertex.name.root()));
206                across.insert(v, spec.edges.len() - 1);
207            }
208            (n, _) => {
209                return Err(GeopError::new(format!(
210                    "the sheet's boundary meets itself at vertex {}: {n} boundary edges end there, where two must",
211                    vertex.name.root()
212                )))
213                .with_context(ctx);
214            }
215        }
216    }
217
218    let coedges = |lp: &[SheetCoedge], side: usize| -> Vec<CoedgeSpec> {
219        lp.iter()
220            .map(|c| CoedgeSpec {
221                on: CoedgeOn::Edge(2 * c.edge + side, c.sense),
222                pcurve: c.pcurve.clone(),
223            })
224            .collect()
225    };
226    for face in &sheet.faces {
227        // `∂u × ∂v` points from the A side to the B side: out of the
228        // material on the B side, into it on the A side.
229        let b = FaceSpec {
230            surface: face.b.clone(),
231            outer: coedges(&face.outer, 1),
232            holes: face.holes.iter().map(|h| coedges(h, 1)).collect(),
233        };
234        let a = FaceSpec {
235            surface: face.a.clone(),
236            outer: coedges(&face.outer, 0),
237            holes: face.holes.iter().map(|h| coedges(h, 0)).collect(),
238        }
239        .reversed();
240        spec.faces.push(a);
241        names.faces.push(rename(face.name.name(&["a"])));
242        spec.faces.push(b);
243        names.faces.push(rename(face.name.name(&["b"])));
244    }
245
246    // The walls. An edge of a face's loop runs with the face on its left,
247    // seen from the B side, so its wall — `u` along the curve, `v` from the
248    // A side to the B side — faces out of the material where the loop runs
249    // the edge forwards, and is turned around where it runs it backwards.
250    let uses = sheet.uses();
251    let point = |u: Scalar, v: Scalar| Vector2::from_array([u, v]);
252    for (e, edge) in sheet.edges.iter().enumerate() {
253        if !boundary[e] {
254            continue;
255        }
256        let (t0, t1) = edge.a.domain();
257        let face = FaceSpec {
258            // The ruled surface between its A and B curves, exact: the two
259            // are of one degree, knots and weights.
260            surface: NurbSurface3D::ruled_compatible(&edge.a, &edge.b)
261                .map_err(|err| err.with_context(format!("the wall of edge {}", edge.name.root())))
262                .with_context(ctx)?,
263            outer: vec![
264                CoedgeSpec {
265                    on: CoedgeOn::Edge(2 * e, Sense::Forward),
266                    pcurve: NurbCurve2D::line(point(t0, Scalar::ZERO), point(t1, Scalar::ZERO))?,
267                },
268                CoedgeSpec {
269                    on: CoedgeOn::Edge(across[&edge.end], Sense::Forward),
270                    pcurve: NurbCurve2D::line(point(t1, Scalar::ZERO), point(t1, Scalar::ONE))?,
271                },
272                CoedgeSpec {
273                    on: CoedgeOn::Edge(2 * e + 1, Sense::Reversed),
274                    pcurve: NurbCurve2D::line(point(t1, Scalar::ONE), point(t0, Scalar::ONE))?,
275                },
276                CoedgeSpec {
277                    on: CoedgeOn::Edge(across[&edge.start], Sense::Reversed),
278                    pcurve: NurbCurve2D::line(point(t0, Scalar::ONE), point(t0, Scalar::ZERO))?,
279                },
280            ],
281            holes: Vec::new(),
282        };
283        spec.faces.push(match uses[e][0].1 {
284            Sense::Forward => face,
285            Sense::Reversed => face.reversed(),
286        });
287        names.faces.push(rename(edge.name.root()));
288    }
289    spec.shells = vec![(0..spec.faces.len()).collect()];
290    model.build_body(spec, names).with_context(ctx)
291}