Skip to main content

brep_kernel/construction/
surface_faces.rs

1//! Faces on given surfaces, and the solid a set of such faces bounds.
2//!
3//! [`face_from_surface`] makes a sheet of one face on a surface over its whole
4//! parameter domain. The face's four sides are the surface's boundary
5//! iso-curves, read for what they are:
6//!
7//! - a side that is one point (every pole of the row coincides) is a POLE, a
8//!   degenerate edge at a vertex;
9//! - two opposite sides that coincide are a SEAM, one edge the face uses once
10//!   each way (a cylinder's side has one seam, a ball's one seam and two
11//!   poles, a ring's two seams);
12//! - any other side is an open edge of the sheet.
13//!
14//! [`face_from_surface_trimmed`] makes a sheet of one face on a surface
15//! bounded by closed loops of curves lying on it (a disc on a plane inside a
16//! circle, a wall on a plane under a spline).
17//!
18//! [`solid_from_sheets`] joins sheets along their coinciding edges (the
19//! kernel's sewing, [`crate::sew_solid`]) and returns the solid they bound,
20//! outward. Sheets that leave an edge open are refused by name; nothing is
21//! closed by guessing.
22use crate::topology::{CoedgeRecord, EdgeRecord, FaceRecord, LoopRecord, ShellRecord, VertexRecord};
23use crate::{
24    build_pcurve_on_surface, make_line, sew_solid, BodyKind, BrepSolid, KernelRefusal, KernelStage, NurbsCurve,
25    NurbsSurface, OrRefuse, Vec3,
26};
27
28/// The prefix of every refusal of this module.
29pub const SURFACE_FACE_REFUSAL: &str = "surfaceFace";
30
31fn input(what: &'static str, message: String) -> KernelRefusal {
32    KernelRefusal::input(KernelStage::Collect, what, format!("{SURFACE_FACE_REFUSAL}: {message}"))
33}
34
35fn refuse(stage: KernelStage, what: &'static str, message: String) -> KernelRefusal {
36    KernelRefusal::unsupported(stage, what, format!("{SURFACE_FACE_REFUSAL}: {message}"))
37}
38
39/// The records of a sheet under construction, ids counted from 1.
40struct Records {
41    vertices: Vec<VertexRecord>,
42    edges: Vec<EdgeRecord>,
43    next: u64,
44    tolerance: f64,
45}
46
47impl Records {
48    fn new(tolerance: f64) -> Self {
49        Self { vertices: Vec::new(), edges: Vec::new(), next: 0, tolerance }
50    }
51
52    fn id(&mut self) -> u64 {
53        self.next += 1;
54        self.next
55    }
56
57    /// The vertex at `point`: an earlier one within the tolerance, or a new one.
58    fn vertex(&mut self, point: Vec3) -> u64 {
59        if let Some(found) = self.vertices.iter().find(|vertex| vertex.point.sub(point).length() <= self.tolerance) {
60            return found.id;
61        }
62        let id = self.id();
63        self.vertices.push(VertexRecord { id, point });
64        id
65    }
66
67    fn edge(&mut self, curve: NurbsCurve, start: u64, end: u64, degenerate: bool) -> Result<u64, KernelRefusal> {
68        let [t0, t1] = curve.domain().or_refuse(KernelStage::Collect, "surfaceFace.edge_domain")?;
69        let id = self.id();
70        self.edges.push(EdgeRecord { id, curve, t0, t1, start_vertex_id: start, end_vertex_id: end, degenerate, name: None });
71        Ok(id)
72    }
73
74    fn sheet(mut self, surface: &NurbsSurface, loops: Vec<Vec<(u64, bool, NurbsCurve)>>) -> BrepSolid {
75        let mut records = Vec::with_capacity(loops.len());
76        for coedges in loops {
77            let coedges = coedges
78                .into_iter()
79                .map(|(edge_id, forward, pcurve)| CoedgeRecord { id: self.id(), edge_id, forward, pcurve })
80                .collect();
81            records.push(LoopRecord { id: self.id(), coedges });
82        }
83        let face = FaceRecord { id: self.id(), surface: surface.clone(), same_sense: true, loops: records, name: None };
84        let shell = ShellRecord { id: self.id(), faces: vec![face] };
85        BrepSolid {
86            mass_properties_cache: Default::default(),
87            id: self.id(),
88            vertices: self.vertices,
89            edges: self.edges,
90            shells: vec![shell],
91            genus: 0,
92            kind: BodyKind::Sheet,
93            cells: Default::default(),
94        }
95    }
96}
97
98/// A length the surface's extent sets: the diagonal of its poles' box.
99fn extent(surface: &NurbsSurface) -> f64 {
100    let (mut low, mut high) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
101    for pole in surface.control_points.iter().flatten() {
102        let point = [pole.x / pole.w, pole.y / pole.w, pole.z / pole.w];
103        for axis in 0..3 {
104            low[axis] = low[axis].min(point[axis]);
105            high[axis] = high[axis].max(point[axis]);
106        }
107    }
108    Vec3::new(high[0] - low[0], high[1] - low[1], high[2] - low[2]).length()
109}
110
111/// The point every pole of `curve` sits at, when they all do within `tolerance`.
112fn collapsed(curve: &NurbsCurve, tolerance: f64) -> Option<Vec3> {
113    let point = |index: usize| {
114        let pole = curve.control_points[index];
115        Vec3::new(pole.x / pole.w, pole.y / pole.w, pole.z / pole.w)
116    };
117    let first = point(0);
118    (1..curve.control_points.len()).all(|index| point(index).sub(first).length() <= tolerance).then_some(first)
119}
120
121/// Whether `a` and `b`, two opposite sides of one patch, are the same curve
122/// point for point (they share knots and degree, so sample at equal parameters).
123fn coincide(a: &NurbsCurve, b: &NurbsCurve, tolerance: f64) -> Result<bool, KernelRefusal> {
124    const SAMPLES: usize = 33;
125    let [t0, t1] = a.domain().or_refuse(KernelStage::Collect, "surfaceFace.side_domain")?;
126    for index in 0..=SAMPLES {
127        let t = t0 + (t1 - t0) * index as f64 / SAMPLES as f64;
128        let gap = a.evaluate(t).or_refuse(KernelStage::Collect, "surfaceFace.side")?
129            .sub(b.evaluate(t).or_refuse(KernelStage::Collect, "surfaceFace.side")?);
130        if gap.length() > tolerance {
131            return Ok(false);
132        }
133    }
134    Ok(true)
135}
136
137fn uv_line(from: [f64; 2], to: [f64; 2]) -> Result<NurbsCurve, KernelRefusal> {
138    make_line(Vec3::new(from[0], from[1], 0.0), Vec3::new(to[0], to[1], 0.0)).or_refuse(KernelStage::Collect, "surfaceFace.pcurve")
139}
140
141fn accept(sheet: BrepSolid, what: &'static str) -> Result<BrepSolid, KernelRefusal> {
142    let issues = sheet.validate();
143    if let Some(issue) = issues.first() {
144        return Err(refuse(KernelStage::Validate, what, format!("the face is not valid: {issue:?}")));
145    }
146    Ok(sheet)
147}
148
149/// A sheet of one face on `surface` over its whole domain, its normal the
150/// surface's own (`∂S/∂u × ∂S/∂v`).
151///
152/// Each side of the domain is read from the surface: a side collapsed to a
153/// point is a pole (a degenerate edge), two opposite sides that coincide are
154/// one seam edge, and every other side is an open edge. So a clamped patch
155/// gives a face with four open edges, a tube's side a face with a seam and two
156/// open rims, and a ball or a ring a closed face, which [`solid_from_sheets`]
157/// takes alone.
158///
159/// Refuses a surface that is a point or a curve (two adjacent sides
160/// collapsed, or a side collapsed whose neighbours coincide all along).
161pub fn face_from_surface(surface: &NurbsSurface) -> Result<BrepSolid, KernelRefusal> {
162    let [u0, u1] = surface.domain_u().or_refuse(KernelStage::Collect, "surfaceFace.domain")?;
163    let [v0, v1] = surface.domain_v().or_refuse(KernelStage::Collect, "surfaceFace.domain")?;
164    let size = extent(surface);
165    if !(size.is_finite() && size > 0.0) {
166        return Err(input("surface_extent", "the surface has no extent".into()));
167    }
168    let tolerance = 1e-9 * size;
169    let side = |along_u: bool, at: f64| -> Result<NurbsCurve, KernelRefusal> {
170        if along_u { surface.iso_curve_v(at) } else { surface.iso_curve_u(at) }.or_refuse(KernelStage::Collect, "surfaceFace.side")
171    };
172    // the four sides, each along its own rising parameter
173    let (bottom, top) = (side(true, v0)?, side(true, v1)?);
174    let (left, right) = (side(false, u0)?, side(false, u1)?);
175    let poles = [collapsed(&bottom, tolerance), collapsed(&right, tolerance), collapsed(&top, tolerance), collapsed(&left, tolerance)];
176    if (0..4).any(|index| poles[index].is_some() && poles[(index + 1) % 4].is_some()) {
177        return Err(input("surface_collapsed", "two adjacent sides of the surface are single points".into()));
178    }
179    let seam_u = poles[1].is_none() && poles[3].is_none() && coincide(&left, &right, tolerance)?;
180    let seam_v = poles[0].is_none() && poles[2].is_none() && coincide(&bottom, &top, tolerance)?;
181
182    let mut records = Records::new(tolerance);
183    let corner = |u: f64, v: f64| surface.evaluate(u, v).or_refuse(KernelStage::Collect, "surfaceFace.corner");
184    let c00 = records.vertex(corner(u0, v0)?);
185    let c10 = records.vertex(corner(u1, v0)?);
186    let c11 = records.vertex(corner(u1, v1)?);
187    let c01 = records.vertex(corner(u0, v1)?);
188    let edge = |records: &mut Records, curve: NurbsCurve, pole: Option<Vec3>, start: u64, end: u64| -> Result<u64, KernelRefusal> {
189        match pole {
190            Some(point) => {
191                let line = make_line(point, point).or_refuse(KernelStage::Collect, "surfaceFace.pole")?;
192                records.edge(line, start, start, true)
193            }
194            None => records.edge(curve, start, end, false),
195        }
196    };
197    let bottom_edge = edge(&mut records, bottom, poles[0], c00, c10)?;
198    let left_edge = edge(&mut records, left, poles[3], c00, c01)?;
199    let top_edge = if seam_v { bottom_edge } else { edge(&mut records, top, poles[2], c01, c11)? };
200    let right_edge = if seam_u { left_edge } else { edge(&mut records, right, poles[1], c10, c11)? };
201    // counter-clockwise in (u, v), so the face lies on the left of each coedge
202    // as seen along the surface's normal
203    let coedges = vec![
204        (bottom_edge, true, uv_line([u0, v0], [u1, v0])?),
205        (right_edge, true, uv_line([u1, v0], [u1, v1])?),
206        (top_edge, false, uv_line([u1, v1], [u0, v1])?),
207        (left_edge, false, uv_line([u0, v1], [u0, v0])?),
208    ];
209    accept(records.sheet(surface, vec![coedges]), "face")
210}
211
212/// The area a closed chain of pcurves encloses in the parameter plane, signed
213/// (positive counter-clockwise), from a sampled polygon.
214fn signed_area(pcurves: &[NurbsCurve]) -> Result<f64, KernelRefusal> {
215    const SAMPLES: usize = 48;
216    let mut points = Vec::with_capacity(pcurves.len() * SAMPLES);
217    for pcurve in pcurves {
218        let [t0, t1] = pcurve.domain().or_refuse(KernelStage::Collect, "surfaceFace.pcurve_domain")?;
219        for index in 0..SAMPLES {
220            let at = pcurve.evaluate(t0 + (t1 - t0) * index as f64 / SAMPLES as f64).or_refuse(KernelStage::Collect, "surfaceFace.pcurve")?;
221            points.push([at.x, at.y]);
222        }
223    }
224    let mut twice = 0.0;
225    for index in 0..points.len() {
226        let (a, b) = (points[index], points[(index + 1) % points.len()]);
227        twice += a[0] * b[1] - b[0] * a[1];
228    }
229    Ok(0.5 * twice)
230}
231
232/// A sheet of one face on `surface` bounded by `loops`: the first its outer
233/// boundary, the rest holes. Each loop is a closed chain of curves lying on
234/// the surface, head to tail in either sense (a single closed curve is a loop
235/// of one); each curve becomes an edge and each joint a vertex. The face's
236/// normal is the surface's own.
237///
238/// The curves' images in the surface's parameters are the kernel's
239/// ([`crate::build_pcurve_on_surface`]: exact on a plane, fitted elsewhere).
240/// The loops are turned the way a face needs them, the outer one
241/// counter-clockwise about the normal and the holes clockwise.
242///
243/// Refuses an empty loop, a chain that does not close, and a curve that does
244/// not lie on the surface.
245pub fn face_from_surface_trimmed(surface: &NurbsSurface, loops: &[Vec<NurbsCurve>]) -> Result<BrepSolid, KernelRefusal> {
246    if loops.is_empty() || loops.iter().any(Vec::is_empty) {
247        return Err(input("trim_no_loop", "at least one loop, each of at least one curve, is required".into()));
248    }
249    let size = extent(surface);
250    if !(size.is_finite() && size > 0.0) {
251        return Err(input("surface_extent", "the surface has no extent".into()));
252    }
253    let tolerance = 1e-9 * size;
254    let ends = |curve: &NurbsCurve| -> Result<[Vec3; 2], KernelRefusal> {
255        let [t0, t1] = curve.domain().or_refuse(KernelStage::Collect, "surfaceFace.curve_domain")?;
256        Ok([curve.evaluate(t0).or_refuse(KernelStage::Collect, "surfaceFace.curve")?, curve.evaluate(t1).or_refuse(KernelStage::Collect, "surfaceFace.curve")?])
257    };
258    let mut records = Records::new(tolerance);
259    let mut face_loops = Vec::with_capacity(loops.len());
260    for (loop_index, curves) in loops.iter().enumerate() {
261        let mut coedges = Vec::with_capacity(curves.len());
262        for (index, curve) in curves.iter().enumerate() {
263            let [start, end] = ends(curve)?;
264            let [next, _] = ends(&curves[(index + 1) % curves.len()])?;
265            if end.sub(next).length() > tolerance {
266                return Err(input("trim_open_loop", format!("loop {loop_index} does not close after curve {index} (gap {:.3e})", end.sub(next).length())));
267            }
268            let (from, to) = (records.vertex(start), records.vertex(end));
269            let pcurve = build_pcurve_on_surface(surface, curve).map_err(|error| {
270                refuse(KernelStage::Fragment, "trim_off_surface", format!("curve {index} of loop {loop_index} has no image on the surface: {error}"))
271            })?;
272            coedges.push((records.edge(curve.clone(), from, to, false)?, true, pcurve));
273        }
274        let pcurves: Vec<NurbsCurve> = coedges.iter().map(|(_, _, pcurve)| pcurve.clone()).collect();
275        let area = signed_area(&pcurves)?;
276        if area == 0.0 {
277            return Err(input("trim_no_area", format!("loop {loop_index} encloses no area on the surface")));
278        }
279        // the outer loop runs counter-clockwise, a hole clockwise
280        if (area > 0.0) != (loop_index == 0) {
281            let mut turned = Vec::with_capacity(coedges.len());
282            for (edge, _, pcurve) in coedges.into_iter().rev() {
283                turned.push((edge, false, pcurve.reversed().or_refuse(KernelStage::Collect, "surfaceFace.pcurve_reverse")?));
284            }
285            coedges = turned;
286        }
287        face_loops.push(coedges);
288    }
289    accept(records.sheet(surface, face_loops), "trimmed_face")
290}
291
292/// The solid `sheets` bound: their faces joined along the edges that coincide
293/// within `tolerance` (the kernel's sewing, [`crate::sew_solid`], which pairs
294/// edges by where they lie and needs no agreement between the sheets'
295/// normals), then turned outward.
296///
297/// One closed face is a solid by itself (a ball's or a ring's face from
298/// [`face_from_surface`]). Refuses sheets that leave an edge open after
299/// sewing, saying how many, and a joined body the kernel's validation rejects.
300pub fn solid_from_sheets(sheets: &[BrepSolid], tolerance: f64) -> Result<BrepSolid, KernelRefusal> {
301    if sheets.is_empty() || sheets.iter().any(|sheet| sheet.shells.iter().all(|shell| shell.faces.is_empty())) {
302        return Err(input("sheets_empty", "at least one sheet, each of at least one face, is required".into()));
303    }
304    if !(tolerance.is_finite() && tolerance > 0.0) {
305        return Err(input("sheets_tolerance", "the sewing tolerance must be positive".into()));
306    }
307    // one body of all the faces, every id made new
308    let mut next = 0u64;
309    let mut id = || {
310        next += 1;
311        next
312    };
313    let (mut vertices, mut edges, mut shells) = (Vec::new(), Vec::new(), Vec::new());
314    for sheet in sheets {
315        let vertex_ids: std::collections::HashMap<u64, u64> = sheet.vertices.iter().map(|vertex| (vertex.id, id())).collect();
316        let edge_ids: std::collections::HashMap<u64, u64> = sheet.edges.iter().map(|edge| (edge.id, id())).collect();
317        let renamed = |ids: &std::collections::HashMap<u64, u64>, old: u64| {
318            ids.get(&old).copied().ok_or_else(|| input("sheets_reference", format!("a sheet refers to the missing id {old}")))
319        };
320        for vertex in &sheet.vertices {
321            vertices.push(VertexRecord { id: vertex_ids[&vertex.id], point: vertex.point });
322        }
323        for edge in &sheet.edges {
324            let mut record = edge.clone();
325            record.id = edge_ids[&edge.id];
326            record.start_vertex_id = renamed(&vertex_ids, edge.start_vertex_id)?;
327            record.end_vertex_id = renamed(&vertex_ids, edge.end_vertex_id)?;
328            edges.push(record);
329        }
330        for shell in &sheet.shells {
331            let mut faces = Vec::with_capacity(shell.faces.len());
332            for face in &shell.faces {
333                let mut record = face.clone();
334                record.id = id();
335                for loop_record in &mut record.loops {
336                    loop_record.id = id();
337                    for coedge in &mut loop_record.coedges {
338                        coedge.id = id();
339                        coedge.edge_id = renamed(&edge_ids, coedge.edge_id)?;
340                    }
341                }
342                faces.push(record);
343            }
344            shells.push(ShellRecord { id: id(), faces });
345        }
346    }
347    let joined = BrepSolid {
348        mass_properties_cache: Default::default(),
349        id: id(),
350        vertices,
351        edges,
352        shells,
353        genus: 0,
354        kind: BodyKind::Sheet,
355        cells: Default::default(),
356    };
357    let (mut solid, report) = sew_solid(&joined, tolerance)?;
358    // a pole's degenerate edge bounds its one face by nature; every other edge must bound two
359    let mut uses = std::collections::HashMap::<u64, usize>::new();
360    for coedge in solid.shells.iter().flat_map(|shell| &shell.faces).flat_map(|face| &face.loops).flat_map(|record| &record.coedges) {
361        *uses.entry(coedge.edge_id).or_default() += 1;
362    }
363    let open = solid.edges.iter().filter(|edge| !edge.degenerate && uses.get(&edge.id).copied().unwrap_or(0) < 2).count();
364    if open > 0 {
365        return Err(refuse(
366            KernelStage::Sew,
367            "sheets_open",
368            format!("the sheets do not close: {open} edge(s) bound one face only after sewing ({} sewn)", report.edges_sewn),
369        ));
370    }
371    solid.kind = BodyKind::Solid;
372    // V − E + F − H = 2·(shells) − 2g, poles' degenerate edges not counted
373    let live = solid.edges.iter().filter(|edge| !edge.degenerate).count() as i64;
374    let faces = solid.shells.iter().map(|shell| shell.faces.len()).sum::<usize>() as i64;
375    let rings = solid.shells.iter().flat_map(|shell| &shell.faces).map(|face| face.loops.len().saturating_sub(1)).sum::<usize>() as i64;
376    let twice = 2 * solid.shells.len() as i64 - (solid.vertices.len() as i64 - live + faces - rings);
377    if twice < 0 || twice % 2 != 0 {
378        return Err(refuse(KernelStage::Validate, "sheets_euler", format!("the joined faces' counts describe no closed surface (2g = {twice})")));
379    }
380    solid.genus = twice / 2;
381    let volume = crate::solid_signed_volume(&solid).or_refuse(KernelStage::Validate, "surfaceFace.volume")?;
382    if volume < 0.0 {
383        crate::offset_shell::flip_all_faces(&mut solid)?;
384    }
385    let issues = solid.validate();
386    if let Some(issue) = issues.first() {
387        return Err(refuse(KernelStage::Validate, "sheets_invalid", format!("the joined faces are not a valid solid: {issue:?}")));
388    }
389    Ok(solid)
390}
391