1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
24use ogeom_topo::{
25 EdgeRepr, Filter, Model, NodeData, Shape, ShapeType, Triangulation, explore_unique,
26};
27
28use crate::discretize::{Deflection, discretize};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct Tessellated {
33 pub faces: usize,
35 pub edges: usize,
37 pub triangles: usize,
39 pub deflection_met: bool,
44}
45
46pub fn tessellate(
58 model: &mut Model,
59 shape: &Shape,
60 deflection: Deflection,
61 tol: Tolerances,
62) -> OgeomResult<Tessellated> {
63 deflection.validate()?;
64 let mut done = Tessellated {
65 faces: 0,
66 edges: 0,
67 triangles: 0,
68 deflection_met: true,
69 };
70
71 ogeom_core::progress::stage("tessellate: faces");
76 let faces: Vec<Shape> = ogeom_topo::explore(model, shape, Filter::OfType(ShapeType::Face))?
77 .into_iter()
78 .collect();
79 let (meshes, chords) = crate::triangulate::face_meshes(model, &faces, deflection, tol)?;
80 let along = |edge: &Shape| -> Deflection {
81 match chords.get(&edge.node().index()) {
82 Some(chord) => Deflection {
83 chord: *chord,
84 ..deflection
85 },
86 None => deflection,
87 }
88 };
89
90 ogeom_core::progress::stage("tessellate: edges");
94 let edges = explore_unique(model, shape, ShapeType::Edge)?;
95 let edge_total = edges.len() as u64;
96 for (at, edge) in edges.into_iter().enumerate() {
97 ogeom_core::progress::checkpoint()?;
98 ogeom_core::progress::stage_at("tessellate: edges", at as u64 + 1, edge_total);
99 if attach_polyline(model, &edge, along(&edge), tol)? {
100 done.edges += 1;
101 }
102 }
103
104 let read_model: &Model = model;
111 let face_total = faces.len() as u64;
116 let faces_done = std::sync::atomic::AtomicU64::new(0);
117 type FaceWork = (Triangulation, Vec<(Shape, Vec<u32>)>);
118 let jobs: Vec<(&Shape, std::sync::Mutex<Option<OgeomResult<Triangulation>>>)> = faces
122 .iter()
123 .zip(meshes)
124 .map(|(face, mesh)| (face, std::sync::Mutex::new(Some(mesh))))
125 .collect();
126 let computed: Vec<OgeomResult<FaceWork>> =
127 ogeom_core::parallel::map_ordered(&jobs, |_, (face, slot)| {
128 ogeom_core::progress::checkpoint()?;
129 let face: &Shape = face;
130 let mesh = slot
131 .lock()
132 .ok()
133 .and_then(|mut held| held.take())
134 .unwrap_or_else(|| {
135 Err(ogeom_core::ogeom_err!(
136 Construction,
137 "a face's mesh was taken twice"
138 ))
139 })?;
140 ogeom_core::progress::stage_at(
141 "tessellate: faces",
142 faces_done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1,
143 face_total,
144 );
145
146 let mut paths: Vec<(Shape, Vec<u32>)> = Vec::new();
150 let mut seen: Vec<(ogeom_topo::TShapeId, ogeom_topo::Location)> = Vec::new();
151 for edge in ogeom_topo::explore(read_model, face, Filter::OfType(ShapeType::Edge))? {
152 let key = (edge.node(), edge.location().clone());
153 if seen.contains(&key) {
154 continue;
155 }
156 seen.push(key);
157 let points =
158 crate::triangulate::polyline_of_edge(read_model, &edge, along(&edge), tol)?;
159 if points.len() < 2 {
160 continue;
161 }
162 if let Some(indices) =
163 index_path(&mesh, &points, edge_reach(read_model, &edge, tol))
164 {
165 paths.push((edge, indices));
166 }
167 }
168 Ok((mesh, paths))
169 });
170
171 for (face, work) in faces.iter().zip(computed) {
172 let face = face.clone();
173 let (mesh, paths) = work?;
174 done.triangles += mesh.triangle_count();
175 done.deflection_met &= mesh.deflection_met;
176
177 let id = model.geometry_mut().add_triangulation(mesh);
178 for (edge, indices) in paths {
179 let Some(node) = model.node_mut(&edge) else {
180 continue;
181 };
182 let NodeData::Edge(data) = node.data_mut() else {
183 continue;
184 };
185 data.representations.push(EdgeRepr::PolygonOnTriangulation {
186 triangulation: id,
187 indices,
188 location: edge.location().clone(),
189 });
190 }
191
192 let Some(node) = model.node_mut(&face) else {
193 ogeom_bail!(Dangling, "face is not in this model");
194 };
195 let NodeData::Face(data) = node.data_mut() else {
196 ogeom_bail!(Construction, "face node holds no face data");
197 };
198 data.triangulation = Some(id);
199 done.faces += 1;
200 }
201 Ok(done)
202}
203
204fn edge_reach(model: &Model, edge: &Shape, tol: Tolerances) -> f64 {
207 let recorded = model
208 .node(edge)
209 .and_then(|n| n.data().as_edge())
210 .map_or(0.0, |d| d.tolerance.get());
211 recorded.max(tol.confusion() * 1e3)
212}
213
214fn index_path(mesh: &Triangulation, points: &[ogeom_math::Point], reach: f64) -> Option<Vec<u32>> {
224 use std::collections::{HashMap, HashSet};
225 let mut by_bits: HashMap<[u64; 3], Vec<u32>> = HashMap::new();
226 for (i, p) in mesh.positions.iter().enumerate() {
227 #[allow(clippy::cast_possible_truncation)]
228 by_bits
229 .entry([p.x.to_bits(), p.y.to_bits(), p.z.to_bits()])
230 .or_default()
231 .push(i as u32);
232 }
233 let mut adjacent: HashSet<(u32, u32)> = HashSet::new();
234 for t in &mesh.triangles {
235 for i in 0..3 {
236 let (a, b) = (t[i], t[(i + 1) % 3]);
237 adjacent.insert((a.min(b), a.max(b)));
238 }
239 }
240 let candidates = |p: &ogeom_math::Point| -> Vec<u32> {
241 if let Some(exact) = by_bits.get(&[p.x.to_bits(), p.y.to_bits(), p.z.to_bits()]) {
242 return exact.clone();
243 }
244 let mut near: Vec<(f64, u32)> = Vec::new();
245 for (i, q) in mesh.positions.iter().enumerate() {
246 let d = q.distance(*p);
247 if d <= reach {
248 #[allow(clippy::cast_possible_truncation)]
249 near.push((d, i as u32));
250 }
251 }
252 near.sort_by(|a, b| a.0.total_cmp(&b.0));
253 near.into_iter().map(|(_, i)| i).collect()
254 };
255
256 let walk = |start: u32| -> Option<Vec<u32>> {
257 let mut out = vec![start];
258 for p in &points[1..] {
259 let previous = *out.last()?;
260 let next = candidates(p)
261 .into_iter()
262 .find(|&c| adjacent.contains(&(previous.min(c), previous.max(c))))?;
263 out.push(next);
264 }
265 Some(out)
266 };
267 candidates(points.first()?).into_iter().find_map(walk)
268}
269
270#[must_use]
272pub fn triangulation_of<'a>(model: &'a Model, face: &Shape) -> Option<&'a Triangulation> {
273 let NodeData::Face(data) = model.node(face)?.data() else {
274 return None;
275 };
276 model.geometry().triangulation(data.triangulation?)
277}
278
279#[must_use]
281pub fn polyline_of(model: &Model, edge: &Shape) -> Option<(Vec<ogeom_math::Point>, Vec<f64>)> {
282 let NodeData::Edge(data) = model.node(edge)?.data() else {
283 return None;
284 };
285 data.representations.iter().find_map(|repr| match repr {
286 EdgeRepr::Polyline {
287 points, parameters, ..
288 } => Some((points.clone(), parameters.clone())),
289 _ => None,
290 })
291}
292
293fn attach_polyline(
298 model: &mut Model,
299 edge: &Shape,
300 deflection: Deflection,
301 tol: Tolerances,
302) -> OgeomResult<bool> {
303 let Some(node) = model.node(edge) else {
304 ogeom_bail!(Dangling, "edge is not in this model");
305 };
306 let NodeData::Edge(data) = node.data() else {
307 ogeom_bail!(Construction, "edge node holds no edge data");
308 };
309 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
310 return Ok(false);
311 };
312 let Some(geometry) = model.geometry().curve(*curve) else {
313 ogeom_bail!(Dangling, "curve is not in this model");
314 };
315 let line = discretize(geometry, *range, deflection, tol)?;
316
317 let Some(node) = model.node_mut(edge) else {
318 ogeom_bail!(Dangling, "edge is not in this model");
319 };
320 let NodeData::Edge(data) = node.data_mut() else {
321 ogeom_bail!(Construction, "edge node holds no edge data");
322 };
323 data.representations.retain(|repr| {
324 !matches!(
325 repr,
326 EdgeRepr::Polyline { .. } | EdgeRepr::PolygonOnTriangulation { .. }
327 )
328 });
329 data.add(EdgeRepr::Polyline {
330 points: line.points,
331 parameters: line.parameters,
332 location: ogeom_topo::Location::identity(),
333 deflection: deflection.chord,
334 });
335 Ok(true)
336}
337
338#[cfg(test)]
339#[allow(clippy::unwrap_used, clippy::expect_used)]
340mod tests {
341 use super::*;
342 use ogeom_algo::make_box;
343 use ogeom_math::Frame;
344
345 const T: Tolerances = Tolerances::millimetres();
346
347 fn fine() -> Deflection {
348 Deflection {
349 chord: 1e-3,
350 angular: 0.05,
351 ..Deflection::default()
352 }
353 }
354
355 #[test]
356 fn tessellating_a_box_stores_a_mesh_on_every_face_and_edge() {
357 let mut model = Model::new();
358 let built = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T).unwrap();
359
360 let done = tessellate(&mut model, &built.shape, fine(), T).unwrap();
361 assert_eq!(done.faces, 6);
362 assert_eq!(done.edges, 12);
363 assert_eq!(done.triangles, 12);
364 assert!(done.deflection_met);
365
366 for face in explore_unique(&model, &built.shape, ShapeType::Face).unwrap() {
367 let mesh = triangulation_of(&model, &face).expect("face has no triangulation");
368 assert_eq!(mesh.triangle_count(), 2);
369 }
370 for edge in explore_unique(&model, &built.shape, ShapeType::Edge).unwrap() {
371 let (points, parameters) = polyline_of(&model, &edge).expect("edge has no polyline");
372 assert_eq!(points.len(), parameters.len());
373 assert_eq!(points.len(), 2, "a straight edge is its own polyline");
374 }
375 }
376
377 #[test]
378 fn the_cached_boundary_agrees_with_the_cached_faces() {
379 let mut model = Model::new();
384 let built = make_box(&mut model, Frame::WORLD, (1.0, 2.0, 3.0), T).unwrap();
385 tessellate(&mut model, &built.shape, fine(), T).unwrap();
386
387 for edge in explore_unique(&model, &built.shape, ShapeType::Edge).unwrap() {
388 let (points, _) = polyline_of(&model, &edge).unwrap();
389 for face in
390 ogeom_topo::ancestors_of(&model, &built.shape, &edge, ShapeType::Face).unwrap()
391 {
392 let mesh = triangulation_of(&model, &face).unwrap();
393 for p in &points {
394 assert!(
395 mesh.positions.iter().any(|q| q.is_equal(*p, T)),
396 "the face's mesh has no vertex at {p:?}, which its edge's \
397 polyline passes through"
398 );
399 }
400 }
401 }
402 }
403
404 #[test]
405 fn a_narrow_face_draws_its_edges_finer_and_its_neighbours_agree() {
406 let mut model = Model::new();
413 let coarse = Deflection {
414 chord: 1.0,
415 ..Deflection::default()
416 };
417 let disc = ogeom_algo::make_cylinder(&mut model, Frame::WORLD, 10.0, 0.5, T).unwrap();
418 tessellate(&mut model, &disc.shape, coarse, T).unwrap();
419
420 let mut refined = 0;
421 for edge in explore_unique(&model, &disc.shape, ShapeType::Edge).unwrap() {
422 let (points, _) = polyline_of(&model, &edge).unwrap();
423 let alone = crate::triangulate::polyline_of_edge(&model, &edge, coarse, T).unwrap();
424 if points.len() > alone.len() {
425 refined += 1;
426 }
427 for face in
428 ogeom_topo::ancestors_of(&model, &disc.shape, &edge, ShapeType::Face).unwrap()
429 {
430 let mesh = triangulation_of(&model, &face).unwrap();
431 for p in &points {
432 assert!(
433 mesh.positions.iter().any(|q| q.is_equal(*p, T)),
434 "the face's mesh has no vertex at {p:?}, which its edge's \
435 polyline passes through"
436 );
437 }
438 }
439 }
440 assert!(
441 refined >= 2,
442 "the rim's circles are drawn finer than the coarse chord asks"
443 );
444 }
445
446 #[test]
447 fn tessellating_again_replaces_rather_than_accumulates() {
448 let mut model = Model::new();
451 let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
452
453 tessellate(&mut model, &built.shape, Deflection::default(), T).unwrap();
454 tessellate(&mut model, &built.shape, fine(), T).unwrap();
455
456 for edge in explore_unique(&model, &built.shape, ShapeType::Edge).unwrap() {
457 let NodeData::Edge(data) = model.node(&edge).unwrap().data() else {
458 unreachable!()
459 };
460 let polylines = data
461 .representations
462 .iter()
463 .filter(|r| matches!(r, EdgeRepr::Polyline { .. }))
464 .count();
465 assert_eq!(polylines, 1, "the earlier polyline was left behind");
466 }
467 }
468
469 #[test]
470 fn a_shape_with_no_faces_tessellates_to_nothing_rather_than_failing() {
471 let mut model = Model::new();
472 let vertex = model.add_point(ogeom_math::Point::ORIGIN);
473 let done = tessellate(&mut model, &vertex, fine(), T).unwrap();
474 assert_eq!(done.faces, 0);
475 assert_eq!(done.edges, 0);
476 assert_eq!(done.triangles, 0);
477 assert!(done.deflection_met);
478 assert!(triangulation_of(&model, &vertex).is_none());
479 assert!(polyline_of(&model, &vertex).is_none());
480 }
481
482 #[test]
483 fn an_unusable_deflection_is_refused_before_anything_is_stored() {
484 let mut model = Model::new();
485 let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
486 let bad = Deflection {
487 chord: 0.0,
488 ..Deflection::default()
489 };
490 assert!(tessellate(&mut model, &built.shape, bad, T).is_err());
491
492 let face = explore_unique(&model, &built.shape, ShapeType::Face).unwrap()[0].clone();
493 assert!(triangulation_of(&model, &face).is_none());
494 }
495}
496#[cfg(test)]
497#[allow(clippy::unwrap_used)]
498mod polygon_on_tests {
499 use super::*;
500 use ogeom_core::Tolerances;
501 use ogeom_math::Frame;
502 use ogeom_topo::{EdgeRepr, Filter, ShapeType, explore};
503
504 const T: Tolerances = Tolerances::millimetres();
505
506 fn fine() -> Deflection {
507 Deflection {
508 chord: 1e-2,
509 ..Deflection::default()
510 }
511 }
512
513 #[test]
514 fn every_edge_walks_its_faces_triangulations_by_index() {
515 let mut model = Model::new();
516 let solid = ogeom_algo::make_cylinder(&mut model, Frame::WORLD, 2.0, 5.0, T).unwrap();
517 tessellate(&mut model, &solid.shape, fine(), T).unwrap();
518
519 let mut checked = 0;
520 for face in explore(&model, &solid.shape, Filter::OfType(ShapeType::Face)).unwrap() {
521 let mesh_id = {
522 let ogeom_topo::NodeData::Face(data) = model.node(&face).unwrap().data() else {
523 panic!("face data");
524 };
525 data.triangulation.unwrap()
526 };
527 let mesh = model.geometry().triangulation(mesh_id).unwrap();
528 let mut edges_of = std::collections::HashSet::new();
530 for t in &mesh.triangles {
531 for i in 0..3 {
532 let (a, b) = (t[i], t[(i + 1) % 3]);
533 edges_of.insert((a.min(b), a.max(b)));
534 }
535 }
536 for edge in explore(&model, &face, Filter::OfType(ShapeType::Edge)).unwrap() {
537 let data = model.node(&edge).unwrap().data().as_edge().unwrap();
538 if data.degenerate {
539 continue;
540 }
541 let paths: Vec<&Vec<u32>> = data
542 .representations
543 .iter()
544 .filter_map(|r| match r {
545 EdgeRepr::PolygonOnTriangulation {
546 triangulation,
547 indices,
548 ..
549 } if *triangulation == mesh_id => Some(indices),
550 _ => None,
551 })
552 .collect();
553 assert!(
554 !paths.is_empty(),
555 "an edge of a tessellated face walks its triangulation"
556 );
557 for indices in paths {
558 assert!(indices.len() >= 2);
559 for pair in indices.windows(2) {
560 let key = (pair[0].min(pair[1]), pair[0].max(pair[1]));
561 assert!(
562 edges_of.contains(&key),
563 "consecutive indices are a triangle edge of the mesh: \
564 {:?} at {:?} and {:?}",
565 pair,
566 mesh.positions[pair[0] as usize],
567 mesh.positions[pair[1] as usize]
568 );
569 }
570 checked += 1;
571 }
572 }
573 }
574 assert!(checked >= 6, "rings, seam sides and rims all walked");
575 }
576}