Skip to main content

geop_ops_subd/
brep.rs

1//! The limit surface of a cage as a B-rep: a solid if the cage is closed,
2//! a sheet if it has a boundary, described whole as a [`BodySpec`].
3//!
4//! The B-rep has the cage's own topology. Every quad of the cage is one
5//! face: the four patches its quads became (see [`crate::limit`]) make one
6//! bicubic B-spline of 7 x 7 control points over `[0, 2]²`, its inner knots
7//! triple. Every edge of the cage is one edge, a cubic of two spans over
8//! `[0, 2]`, and every vertex one vertex, at its limit position. A face of
9//! any other number of sides cannot be one tensor-product surface, so it
10//! stays as the patches it became, one face per corner, joined at its face
11//! point by edges to the middle of each side — where its sides are split.
12//!
13//! Names follow the cage: for the step `Scalar`, the face of cage face `f7` is
14//! `subd(Scalar,f7)`, the edge of cage edge `e3-5` `subd(Scalar,e3-5)`, the vertex of
15//! cage vertex `v3` `subd(Scalar,v3)`, and the solid `subd(Scalar)`. Of an n-gon
16//! `f7`, the face at its corner `v3` is `subd(Scalar,f7,v3)`, its centre
17//! `subd(Scalar,f7,centre)` and the edge from there to the middle of side
18//! `e3-5` `subd(Scalar,f7,e3-5)`; that side's halves are `subd(Scalar,e3-5,v3)` and
19//! `subd(Scalar,e3-5,v5)`, meeting at `subd(Scalar,e3-5,mid)`.
20
21use std::collections::HashMap;
22
23use geop_core_geometry::{
24    nurb_curve::NurbCurve,
25    nurb_surface::{NurbSurface, NurbSurface3D},
26};
27use geop_core_math::{
28    geop_error::{GeopError, GeopResult},
29    scalars::Scalar,
30    vector::Vector3,
31};
32use geop_core_topology::{
33    Curve2, Curve3, Sense,
34    build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
35};
36use geop_ops_model::{BodyNames, Namer};
37
38use crate::{
39    cage::{Mesh, undirected},
40    limit::{ControlNet, control_net},
41    subdivide::subdivide,
42};
43
44/// The clamped cubic knots of `spans` Bézier spans over `[0, spans]`.
45fn cubic_knots(spans: usize) -> Vec<Scalar> {
46    let mut knots = vec![Scalar::ZERO; 4];
47    for s in 1..spans {
48        knots.extend([Scalar::from_i64(s as i64); 3]);
49    }
50    knots.extend([Scalar::from_i64(spans as i64); 4]);
51    knots
52}
53
54/// The cubic through Bézier control points `points`, one span per three.
55fn cubic(points: &[Vector3]) -> GeopResult<Curve3> {
56    NurbCurve::try_new(
57        3,
58        points.iter().map(|p| p.homogeneous(Scalar::ONE)).collect(),
59        cubic_knots((points.len() - 1) / 3),
60    )
61}
62
63/// The straight pcurve from `a` to `b`, over `[0, length]`.
64fn line(a: [i64; 2], b: [i64; 2], length: i64) -> GeopResult<Curve2> {
65    let point = |p: [i64; 2]| {
66        geop_core_math::vector::Vector3::from_array([
67            Scalar::from_i64(p[0]),
68            Scalar::from_i64(p[1]),
69            Scalar::ONE,
70        ])
71    };
72    NurbCurve::try_new(
73        1,
74        vec![point(a), point(b)],
75        vec![
76            Scalar::ZERO,
77            Scalar::ZERO,
78            Scalar::from_i64(length),
79            Scalar::from_i64(length),
80        ],
81    )
82}
83
84/// The B-rep edges a cage edge became.
85#[derive(Clone, Copy, Debug)]
86enum EdgeParts {
87    /// One edge, running from the cage edge's first vertex to its second.
88    Whole(usize),
89    /// Split at its middle: the half from the first vertex, and the half to
90    /// the second.
91    Split(usize, usize),
92}
93
94/// What [`limit_body`] builds the B-rep from.
95struct Builder<'a> {
96    net: &'a ControlNet,
97    namer: &'a Namer,
98    spec: BodySpec,
99    names: BodyNames,
100    /// Per vertex of the subdivided mesh that is one of the B-rep's: its
101    /// index there.
102    vertex: HashMap<usize, usize>,
103}
104
105impl Builder<'_> {
106    fn add_vertex(&mut self, sub_vertex: usize, name: &[&str]) {
107        self.vertex.insert(sub_vertex, self.spec.vertices.len());
108        self.spec.vertices.push(self.net.corner[sub_vertex]);
109        self.names.vertices.push(self.namer.name(name));
110    }
111
112    /// The curve along the subdivided mesh's vertices `path`, from one
113    /// B-rep vertex to another.
114    fn add_edge(&mut self, path: &[usize], name: &[&str]) -> GeopResult<usize> {
115        let mut points = vec![self.net.corner[path[0]]];
116        for w in path.windows(2) {
117            points.push(self.net.edge[&(w[0], w[1])]);
118            points.push(self.net.edge[&(w[1], w[0])]);
119            points.push(self.net.corner[w[1]]);
120        }
121        self.spec.edges.push(EdgeSpec {
122            curve: cubic(&points)?,
123            start: self.vertex[&path[0]],
124            end: self.vertex[&path[path.len() - 1]],
125        });
126        self.names.edges.push(self.namer.name(name));
127        Ok(self.spec.edges.len() - 1)
128    }
129}
130
131/// The coedges of a face's side from cage vertex `from` to `to`, `parts`
132/// the cage edge's B-rep edges, in `(u, v)` from `start` through `middle`
133/// to `end`.
134fn side(
135    parts: EdgeParts,
136    from: usize,
137    to: usize,
138    [start, middle, end]: [[i64; 2]; 3],
139) -> GeopResult<Vec<CoedgeSpec>> {
140    let forward = from < to;
141    let sense = Sense::from_forward(forward);
142    Ok(match parts {
143        EdgeParts::Whole(e) => vec![CoedgeSpec {
144            on: CoedgeOn::Edge(e, sense),
145            pcurve: line(start, end, 2)?,
146        }],
147        EdgeParts::Split(first, second) => {
148            let (near, far) = if forward {
149                (first, second)
150            } else {
151                (second, first)
152            };
153            vec![
154                CoedgeSpec {
155                    on: CoedgeOn::Edge(near, sense),
156                    pcurve: line(start, middle, 1)?,
157                },
158                CoedgeSpec {
159                    on: CoedgeOn::Edge(far, sense),
160                    pcurve: line(middle, end, 1)?,
161                },
162            ]
163        }
164    })
165}
166
167/// Checks that a closed mesh's faces run counter-clockwise seen from
168/// outside: that it encloses a volume that is definitely positive.
169fn check_outward(mesh: &Mesh) -> GeopResult<()> {
170    let p = |v: usize| Vector3::from_array(mesh.positions[v].map(Scalar::from_f64));
171    let mut volume = Scalar::ZERO;
172    for face in &mesh.faces {
173        for k in 1..face.len() - 1 {
174            let (a, b, c) = (p(face[0]), p(face[k]), p(face[k + 1]));
175            volume = volume.add(a.prod_dot(&b.prod_cross(&c)));
176        }
177    }
178    if volume.definitely_greater(Scalar::ZERO) {
179        Ok(())
180    } else {
181        Err(GeopError::new(format!(
182            "the cage encloses no volume, or is turned inside out — its faces must run counter-clockwise seen from outside (six times its volume: {volume:?})"
183        )))
184    }
185}
186
187/// The names of the faces [`limit_body`] builds from `mesh`, in its order:
188/// one per quad, one per corner of any other face.
189pub fn face_names(mesh: &Mesh, namer: &Namer) -> Vec<String> {
190    let mut names = Vec::new();
191    for (f, face) in mesh.faces.iter().enumerate() {
192        if face.len() == 4 {
193            names.push(namer.name(&[&mesh.face_names[f]]));
194        } else {
195            for &v in face {
196                names.push(namer.name(&[&mesh.face_names[f], &mesh.vertex_names[v]]));
197            }
198        }
199    }
200    names
201}
202
203/// The limit surface of `mesh` as a B-rep (see the module docs), named
204/// with `namer`: a solid if the mesh is closed, else a sheet.
205pub fn limit_body(mesh: &Mesh, namer: &Namer) -> GeopResult<(BodySpec, BodyNames)> {
206    let topology = mesh.topology()?;
207    let edges = topology.edges();
208    let closed = edges.values().all(|faces| faces.len() == 2);
209    if closed {
210        check_outward(mesh)?;
211    }
212    let sub = subdivide(mesh, &topology)?;
213    let net = control_net(&sub)?;
214    let mut b = Builder {
215        net: &net,
216        namer,
217        spec: BodySpec::new(closed),
218        names: BodyNames {
219            solid: closed.then(|| namer.root()),
220            ..BodyNames::default()
221        },
222        vertex: HashMap::new(),
223    };
224
225    for v in 0..mesh.positions.len() {
226        b.add_vertex(v, &[&mesh.vertex_names[v]]);
227    }
228    let is_quad = |f: usize| mesh.faces[f].len() == 4;
229    for (f, face) in mesh.faces.iter().enumerate() {
230        if face.len() != 4 {
231            b.add_vertex(sub.face_point[f], &[&mesh.face_names[f], "centre"]);
232        }
233    }
234    let mut parts: HashMap<[usize; 2], EdgeParts> = HashMap::new();
235    for (&[a, c], faces) in &edges {
236        let name = mesh.edge_name(a, c);
237        let m = sub.edge_point[&[a, c]];
238        let part = if faces.iter().all(|&f| is_quad(f)) {
239            EdgeParts::Whole(b.add_edge(&[a, m, c], &[&name])?)
240        } else {
241            b.add_vertex(m, &[&name, "mid"]);
242            EdgeParts::Split(
243                b.add_edge(&[a, m], &[&name, &mesh.vertex_names[a]])?,
244                b.add_edge(&[m, c], &[&name, &mesh.vertex_names[c]])?,
245            )
246        };
247        parts.insert([a, c], part);
248    }
249    let part = |a: usize, c: usize| parts[&undirected(a, c)];
250
251    for (f, face) in mesh.faces.iter().enumerate() {
252        let n = face.len();
253        if n == 4 {
254            let mut grid = [Vector3::zero(); 49];
255            for k in 0..4 {
256                let patch = net.patch(&sub, sub.face_start[f] + k);
257                for (i, row) in patch.iter().enumerate() {
258                    for (j, point) in row.iter().enumerate() {
259                        let (u, v) = match k {
260                            0 => (i, j),
261                            1 => (6 - j, i),
262                            2 => (6 - i, 6 - j),
263                            _ => (j, 6 - i),
264                        };
265                        grid[u * 7 + v] = *point;
266                    }
267                }
268            }
269            let surface = NurbSurface::try_new(
270                3,
271                3,
272                grid.iter().map(|p| p.homogeneous(Scalar::ONE)).collect(),
273                cubic_knots(2),
274                cubic_knots(2),
275            )?;
276            let corners = [[0, 0], [2, 0], [2, 2], [0, 2]];
277            let mut outer = Vec::new();
278            for k in 0..4 {
279                let (from, to) = (face[k], face[(k + 1) % 4]);
280                let (s, e) = (corners[k], corners[(k + 1) % 4]);
281                let middle = [(s[0] + e[0]) / 2, (s[1] + e[1]) / 2];
282                outer.extend(side(part(from, to), from, to, [s, middle, e])?);
283            }
284            b.spec.faces.push(FaceSpec {
285                surface,
286                outer,
287                holes: Vec::new(),
288            });
289        } else {
290            let centre = sub.face_point[f];
291            let spokes = (0..n)
292                .map(|k| {
293                    let (a, c) = (face[k], face[(k + 1) % n]);
294                    let m = sub.edge_point[&undirected(a, c)];
295                    b.add_edge(&[centre, m], &[&mesh.face_names[f], &mesh.edge_name(a, c)])
296                })
297                .collect::<GeopResult<Vec<_>>>()?;
298            for k in 0..n {
299                let (v, after, before) = (face[k], face[(k + 1) % n], face[(k + n - 1) % n]);
300                let patch = net.patch(&sub, sub.face_start[f] + k);
301                let surface: NurbSurface3D = NurbSurface::try_new(
302                    3,
303                    3,
304                    patch
305                        .iter()
306                        .flatten()
307                        .map(|p| p.homogeneous(Scalar::ONE))
308                        .collect(),
309                    cubic_knots(1),
310                    cubic_knots(1),
311                )?;
312                let mut outer = Vec::new();
313                // From the corner to the middle of the side after it: the
314                // first half of that side.
315                let ahead = side(part(v, after), v, after, [[0, 0], [1, 0], [2, 0]])?;
316                outer.push(ahead[0].clone());
317                outer.push(CoedgeSpec {
318                    on: CoedgeOn::Edge(spokes[k], Sense::Reversed),
319                    pcurve: line([1, 0], [1, 1], 1)?,
320                });
321                outer.push(CoedgeSpec {
322                    on: CoedgeOn::Edge(spokes[(k + n - 1) % n], Sense::Forward),
323                    pcurve: line([1, 1], [0, 1], 1)?,
324                });
325                // From the middle of the side before it back to the corner:
326                // the second half of that side.
327                let behind = side(part(before, v), before, v, [[0, 2], [0, 1], [0, 0]])?;
328                outer.push(behind[1].clone());
329                b.spec.faces.push(FaceSpec {
330                    surface,
331                    outer,
332                    holes: Vec::new(),
333                });
334            }
335        }
336    }
337    b.names.faces = face_names(mesh, namer);
338    b.spec.shells = vec![(0..b.spec.faces.len()).collect()];
339    Ok((b.spec, b.names))
340}