Skip to main content

geop_core_rasterize/
lib.rs

1//! Turn a [`Model`] into sampled geometry — [`rasterize`]: a point per
2//! vertex, a polyline per edge, triangles per face, each tagged with the
3//! entity it came from — to draw it, pick in it, write it as STL ([`stl`]),
4//! or render it for debugging ([`debug`]).
5//!
6//! Every edge is sampled once ([`sample_curve`]), and every face — trimmed
7//! or not — has its outer boundary (and any holes) made of those samples,
8//! placed in its `(u, v)` (see [`mesh::boundary`]), then triangulated by
9//! [`grid::triangulate_face`]: a curvature-sized `(u, v)` grid with every
10//! cell clipped exactly against the trim (see that module's doc comment),
11//! so a flat patch renders as a couple of triangles and a curved one gets
12//! whatever resolution its own curvature actually needs — mapped through
13//! `surface.evaluate`, the rendered mesh always both respects the face's
14//! real trim and follows its true curvature. The faces' meshes are then
15//! joined along their edges (see [`mesh`]): the mesh of a solid is closed.
16
17mod clip;
18pub mod debug;
19mod grid;
20mod mesh;
21pub mod stl;
22
23pub use grid::triangulate_polygon;
24
25/// A triangle in a plane, by its corners.
26pub type Triangle2 = (Vector2, Vector2, Vector2);
27
28use std::collections::BTreeMap;
29
30use geop_core_geometry::nurb_surface::NurbSurface3D;
31use geop_core_math::{
32    geop_error::{GeopError, GeopResult},
33    plain::distance,
34    primitives::TriangleFace,
35    scalars::Scalar,
36    solvers::refine::{Refine, Refined, refine},
37    vector::{Vector2, Vector3},
38};
39use geop_core_topology::{
40    CoedgeGeometry, CoedgeId, Curve2, Edge, EdgeId, Face, FaceId, Model, Sense, VertexId,
41    boundary::BoundaryType,
42};
43use grid::dist_point_to_segment;
44
45/// `face`'s trimmed region triangulated in `(u, v)` space, by a grid sized
46/// by the face's curvature and clipped to its trim (see
47/// [`grid::triangulate_face`]), each triangle by its corners — and the
48/// trim it covers: its loops as the polygons of its edges' samples, the
49/// outer first.
50pub fn face_triangles_uv(
51    model: &Model,
52    face: &Face,
53    n: usize,
54) -> GeopResult<(Vec<Triangle2>, Vec<Vec<Vector2>>)> {
55    let mut edges = BTreeMap::new();
56    for anchor in loops(face) {
57        for c in model.iterate_loop_coedges(anchor) {
58            if let CoedgeGeometry::Edge(e) = model.get_coedge(c)?.geometry {
59                edges.insert(e, sample_edge(model, e, n)?);
60            }
61        }
62    }
63    let mut samples = grid::Samples::new(face);
64    let (triangles, boundaries) = face_mesh(model, face, n, &edges, &mut samples)?;
65    let at = |p: &clip::Point| Vector2::from_array(p.map(Scalar::from_f64));
66    Ok((
67        triangles
68            .into_iter()
69            .map(|[a, b, c]| (samples.uv(a), samples.uv(b), samples.uv(c)))
70            .collect(),
71        boundaries
72            .iter()
73            .map(|b| b.uv.iter().map(at).collect())
74            .collect(),
75    ))
76}
77
78/// The anchors of `face`'s trim loops, the outer first.
79fn loops(face: &Face) -> Vec<geop_core_topology::CoedgeId> {
80    std::iter::once(&face.outer)
81        .chain(&face.holes)
82        .filter_map(|b| match b {
83            BoundaryType::Loop(anchor) => Some(*anchor),
84            BoundaryType::Vertex(_) => None,
85        })
86        .collect()
87}
88
89/// `face` triangulated in `(u, v)`, trimmed by the polylines `edges` its
90/// edges were sampled into: the triangles, and its trim loops.
91fn face_mesh(
92    model: &Model,
93    face: &Face,
94    n: usize,
95    edges: &BTreeMap<EdgeId, Vec<Vector3>>,
96    samples: &mut grid::Samples,
97) -> GeopResult<(Vec<[clip::Point; 3]>, Vec<mesh::Boundary>)> {
98    let BoundaryType::Loop(_) = face.outer else {
99        return Ok((Vec::new(), Vec::new()));
100    };
101    let boundaries = loops(face)
102        .into_iter()
103        .map(|anchor| mesh::boundary(model, face, anchor, n, edges))
104        .collect::<GeopResult<Vec<_>>>()?;
105    let holes: Vec<Vec<clip::Point>> = boundaries[1..].iter().map(|b| b.uv.clone()).collect();
106    let triangles = grid::triangulate_face(&boundaries[0].uv, &holes, n, samples)?;
107    Ok((triangles, boundaries))
108}
109
110/// Edge `e` of `model` sampled (see [`sample_curve`]), its ends exactly
111/// its vertices' points: the polyline every face it bounds meets along.
112fn sample_edge(model: &Model, e: EdgeId, n: usize) -> GeopResult<Vec<Vector3>> {
113    let edge = model.get_edge(e)?;
114    let mut points = sample_curve(&edge.curve, n)?;
115    let last = points.len() - 1;
116    points[0] = model.get_vertex(edge.start_vertex)?.point;
117    points[last] = model.get_vertex(edge.end_vertex)?.point;
118    Ok(points)
119}
120
121/// A [`Model`] rasterized once, with every sampled point/polyline/triangle
122/// kept alongside the id of the entity it came from.
123///
124/// This is the single place the crate turns topology into sampled geometry
125/// — drawing and ray picking (`geop_core_editor::view::PartView`), STL, and debug
126/// rendering ([`debug`]) all build on top of it, rather than each walking
127/// `Model` and sampling curves/surfaces on their own. That matters beyond
128/// not repeating code: it guarantees a pick can never disagree with what
129/// the viewer actually drew, because both read the same triangles.
130pub struct RasterizedModel {
131    pub vertices: BTreeMap<VertexId, Vector3>,
132    /// Each edge's curve sampled into an `n`-point polyline.
133    pub edges: BTreeMap<EdgeId, Vec<Vector3>>,
134    /// Each face's trimmed region, triangulated (see [`face_triangles_uv`])
135    /// and mapped through `surface.evaluate` — one face generally maps to
136    /// several triangles.
137    pub faces: BTreeMap<FaceId, Vec<TriangleFace>>,
138}
139
140/// A curve is first sampled this finely, and refined from there.
141const MIN_EDGE_SEGMENTS: usize = 1;
142/// How many times an edge's sampling may double.
143const MAX_EDGE_DOUBLINGS: usize = 10;
144
145/// `curve` as a polyline that stays within `quality`'s curvature tolerance
146/// of it: a straight edge is two points, a tight arc gets as many as it
147/// needs. Same rule as [`grid`]'s cells — the tolerance shrinks with
148/// `quality²` because a segment's deviation from the curve does too — so a
149/// model's edges and faces come out equally smooth.
150fn sample_curve(
151    curve: &geop_core_geometry::nurb_curve::NurbCurve3D,
152    quality: usize,
153) -> GeopResult<Vec<Vector3>> {
154    let (t0, t1) = curve.domain();
155    let at = |frac: (usize, usize)| -> GeopResult<Vector3> {
156        let f = Scalar::from_ratio(frac.0 as i64, frac.1 as i64)?;
157        curve.evaluate(t0.add(t1.sub(t0).mul(f)))
158    };
159    let points_at = |segments: usize| -> GeopResult<Vec<Vector3>> {
160        (0..=segments).map(|i| at((i, segments))).collect()
161    };
162
163    let first = points_at(MIN_EDGE_SEGMENTS)?;
164    let size = first
165        .iter()
166        .flat_map(|p| {
167            first
168                .iter()
169                .map(move |q| distance(p.to_f64_array(), q.to_f64_array()))
170        })
171        .fold(0.0f64, f64::max)
172        .max(1e-9);
173    let tolerance = size / (4.0 * (quality * quality) as f64);
174
175    // Doubled while the true point halfway along some segment strays from
176    // its chord by more than the tolerance; at the cap, as fine as it got.
177    let refined = refine(
178        (MIN_EDGE_SEGMENTS, first),
179        MAX_EDGE_DOUBLINGS,
180        |(segments, points)| -> GeopResult<_> {
181            let mut worst = 0.0f64;
182            for i in 0..segments {
183                let mid = at((2 * i + 1, 2 * segments))?;
184                worst = worst.max(dist_point_to_segment(
185                    mid.to_f64_array(),
186                    points[i].to_f64_array(),
187                    points[i + 1].to_f64_array(),
188                ));
189            }
190            if worst <= tolerance {
191                return Ok(Refine::Done(points));
192            }
193            Ok(Refine::Finer((2 * segments, points_at(2 * segments)?)))
194        },
195    )?;
196    Ok(match refined {
197        Refined::Done(points) | Refined::Exhausted((_, points)) => points,
198    })
199}
200
201/// Sample every vertex/edge/face of `model` into a [`RasterizedModel`].
202/// `n` is a quality: how finely a curve or surface that actually curves is
203/// approximated (see [`sample_curve`] and [`grid::triangulate_face`]), not
204/// a fixed sample count — a straight edge or flat face stays cheap.
205pub fn rasterize(model: &Model, n: usize) -> GeopResult<RasterizedModel> {
206    Rasterizer::default().rasterize(model, n)
207}
208
209/// How many models a [`Rasterizer`] keeps the faces of: those of the part
210/// before the step an editor shows, the part with it, and those of the
211/// last steps opened.
212const KEPT_MODELS: u64 = 4;
213
214/// What a face is meshed from on its own ([`face_mesh`]), at a quality:
215/// its surface, and each of its loops, the outer first, as the coedges it
216/// runs through — each one's pcurve and sense, and its edge, with the
217/// edge's ends. Two faces of equal sources are meshed alike, bit for bit,
218/// whatever their shells or their coedges' ids.
219#[derive(PartialEq)]
220struct FaceSource {
221    n: usize,
222    surface: NurbSurface3D,
223    loops: Vec<Vec<CoedgeSource>>,
224}
225
226/// A coedge of a [`FaceSource`]: what it runs along, which way, its pcurve,
227/// and its edge's curve and ends, if it has an edge.
228type CoedgeSource = (CoedgeGeometry, Sense, Curve2, Option<(Edge, [Vector3; 2])>);
229
230impl FaceSource {
231    fn of(model: &Model, face: &Face, n: usize) -> GeopResult<Self> {
232        let coedge = |c: CoedgeId| -> GeopResult<CoedgeSource> {
233            let coedge = model.get_coedge(c)?;
234            let edge = match coedge.geometry {
235                CoedgeGeometry::Edge(e) => {
236                    let edge = model.get_edge(e)?;
237                    let start = model.get_vertex(edge.start_vertex)?.point;
238                    let end = model.get_vertex(edge.end_vertex)?.point;
239                    Some((edge.clone(), [start, end]))
240                }
241                CoedgeGeometry::Vertex(_) => None,
242            };
243            Ok((coedge.geometry, coedge.sense, coedge.pcurve.clone(), edge))
244        };
245        let loops = (loops(face).into_iter())
246            .map(|anchor| model.iterate_loop_coedges(anchor).map(coedge).collect())
247            .collect::<GeopResult<_>>()?;
248        Ok(Self {
249            n,
250            surface: face.surface.clone(),
251            loops,
252        })
253    }
254}
255
256/// A face meshed on its own, before it is joined to its neighbours (see
257/// [`mesh`]): its triangles in `(u, v)`, its trim loops, and its surface
258/// where they sampled it.
259struct Meshed {
260    triangles: Vec<[clip::Point; 3]>,
261    boundaries: Vec<mesh::Boundary>,
262    samples: grid::Samples,
263}
264
265/// Rasterizes models one after another, meshing a face on its own only
266/// once while it stays the same: the part before a step and the part with
267/// it share the faces the step left as they were, and those are not
268/// meshed again. The faces are joined along their edges anew each time,
269/// so what it makes of a model is what [`rasterize`] makes of it.
270#[derive(Default)]
271pub struct Rasterizer {
272    /// How many models it rasterized.
273    models: u64,
274    /// The faces it meshed, by id: from what, and the model that last
275    /// used each — kept while one of the last [`KEPT_MODELS`] did.
276    faces: BTreeMap<FaceId, Vec<(FaceSource, Meshed, u64)>>,
277}
278
279thread_local! {
280    static FACES_MESHED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
281}
282
283/// How many faces this thread has meshed on its own ever: what a
284/// [`Rasterizer`] does not do again for a face that stayed the same — so a
285/// test bounds it by counting these, not by timing it.
286pub fn faces_meshed() -> usize {
287    FACES_MESHED.get()
288}
289
290impl Rasterizer {
291    /// `model` rasterized as [`rasterize`] does, of the faces meshed
292    /// before what stayed the same.
293    pub fn rasterize(&mut self, model: &Model, n: usize) -> GeopResult<RasterizedModel> {
294        if n < 2 {
295            return Err(GeopError::new("rasterize: n must be >= 2"));
296        }
297        self.models += 1;
298        let now = self.models;
299
300        let vertices = model
301            .vertices
302            .iter()
303            .map(|(&id, vertex)| (id, vertex.point))
304            .collect();
305
306        let mut edges = BTreeMap::new();
307        for &id in model.edges.keys() {
308            edges.insert(id, sample_edge(model, id, n)?);
309        }
310        let last: BTreeMap<EdgeId, usize> = edges.iter().map(|(&e, p)| (e, p.len() - 1)).collect();
311
312        // Every face meshed on its own, or as it was, then all joined along
313        // their edges.
314        let mut ids = Vec::new();
315        let mut sources = Vec::new();
316        let mut all_samples = Vec::new();
317        let mut all_boundaries = Vec::new();
318        let mut triangles = Vec::new();
319        for (&id, face) in model.faces.iter() {
320            let source = FaceSource::of(model, face, n)?;
321            let kept = self.faces.entry(id).or_default();
322            let meshed = match kept.iter().position(|(meshed, ..)| *meshed == source) {
323                Some(k) => kept.swap_remove(k).1,
324                None => {
325                    FACES_MESHED.set(FACES_MESHED.get() + 1);
326                    let mut samples = grid::Samples::new(face);
327                    let (triangles, boundaries) = face_mesh(model, face, n, &edges, &mut samples)?;
328                    Meshed {
329                        triangles,
330                        boundaries,
331                        samples,
332                    }
333                }
334            };
335            ids.push(id);
336            sources.push(source);
337            all_samples.push(meshed.samples);
338            all_boundaries.push(meshed.boundaries);
339            triangles.push(meshed.triangles);
340        }
341        let trims: Vec<mesh::Trim> = all_boundaries
342            .iter()
343            .map(|boundaries| mesh::Trim {
344                model,
345                boundaries,
346                last: &last,
347            })
348            .collect();
349        let mut meshes: Vec<mesh::Mesh> = triangles
350            .iter()
351            .zip(&trims)
352            .map(|(tris, trim)| mesh::Mesh::new(tris, trim))
353            .collect::<GeopResult<_>>()?;
354        mesh::zip(&mut meshes, &trims);
355
356        let mut faces = BTreeMap::new();
357        for ((&id, m), samples) in ids.iter().zip(&meshes).zip(&mut all_samples) {
358            let points = (0..m.uv.len())
359                .map(|i| match mesh::placed(model, m.on[i], &edges)? {
360                    Some(p) => Ok(p),
361                    None => samples.point(m.uv[i]),
362                })
363                .collect::<GeopResult<Vec<_>>>()?;
364            let mut tris = Vec::new();
365            for &[a, b, c] in &m.triangles {
366                // A triangle without area — three points along an edge —
367                // has no normal: it covers nothing, and the triangles
368                // beside it meet each other.
369                let Ok(t) = TriangleFace::try_new(points[a], points[b], points[c]) else {
370                    continue;
371                };
372                // The surface's own normal at each corner, for smooth
373                // shading. A corner where it does not exist leaves the
374                // whole triangle flat.
375                let mut normal = |i: usize| samples.normal(m.uv[i]);
376                tris.push(match (normal(a), normal(b), normal(c)) {
377                    (Some(na), Some(nb), Some(nc)) => t.with_vertex_normals([na, nb, nc]),
378                    _ => t,
379                });
380            }
381            faces.insert(id, tris);
382        }
383
384        // Each face kept as it was meshed on its own, with what its surface
385        // was sampled at since, for the next model.
386        let meshed = (triangles.into_iter().zip(all_boundaries).zip(all_samples)).map(
387            |((triangles, boundaries), samples)| Meshed {
388                triangles,
389                boundaries,
390                samples,
391            },
392        );
393        for ((id, source), meshed) in ids.into_iter().zip(sources).zip(meshed) {
394            self.faces
395                .entry(id)
396                .or_default()
397                .push((source, meshed, now));
398        }
399        self.faces.retain(|_, kept| {
400            kept.retain(|(.., used)| used + KEPT_MODELS > now);
401            !kept.is_empty()
402        });
403
404        Ok(RasterizedModel {
405            vertices,
406            edges,
407            faces,
408        })
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415    use crate::debug::Color10;
416    use geop_core_topology::test_fixtures::test_cube_model;
417
418    /// Rasterize `model`, sanity-check it's a non-empty mesh, and save it to
419    /// `outputs/<name>.html` for visual inspection.
420    fn rasterize_and_save(model: &Model, name: &str) {
421        let scene = rasterize(model, 8).unwrap().scene(|_| Color10::Blue);
422        assert!(!scene.points.is_empty());
423        assert!(!scene.lines.is_empty());
424        assert!(!scene.triangles.is_empty());
425        std::fs::create_dir_all("outputs").unwrap();
426        scene.save_to_file(&format!("outputs/{name}.html")).unwrap();
427    }
428
429    #[test]
430    fn rasterize_cube() {
431        rasterize_and_save(&test_cube_model(), "cube");
432    }
433}