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, Polyline};
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(
60 model: &mut Model,
61 shape: &Shape,
62 deflection: Deflection,
63 tol: Tolerances,
64) -> OgeomResult<Tessellated> {
65 deflection.validate()?;
66 model.unshare(shape)?;
67 let mut done = Tessellated {
68 faces: 0,
69 edges: 0,
70 triangles: 0,
71 deflection_met: true,
72 };
73
74 ogeom_core::progress::stage("tessellate: faces");
79 let faces: Vec<Shape> = ogeom_topo::explore(model, shape, Filter::OfType(ShapeType::Face))?
80 .into_iter()
81 .collect();
82 let (meshes, chords) = crate::triangulate::face_meshes(model, &faces, deflection, None, tol)?;
83 let along = |edge: &Shape| -> Deflection {
84 match chords.get(&edge.node().index()) {
85 Some(chord) => Deflection {
86 chord: *chord,
87 ..deflection
88 },
89 None => deflection,
90 }
91 };
92
93 ogeom_core::progress::stage("tessellate: edges");
97 let edges = explore_unique(model, shape, ShapeType::Edge)?;
101 let edge_total = edges.len() as u64;
102 let mut lines: ogeom_core::FastMap<u32, Polyline> = ogeom_core::FastMap::default();
103 for (at, edge) in edges.into_iter().enumerate() {
104 ogeom_core::progress::checkpoint()?;
105 ogeom_core::progress::stage_at("tessellate: edges", at as u64 + 1, edge_total);
106 let deflection = along(&edge);
107 if let Some(line) = edge_line(model, &edge, deflection, tol)? {
108 lines
109 .entry(edge.node().index())
110 .or_insert_with(|| line.clone());
111 store_polyline(model, &edge, line, deflection)?;
112 done.edges += 1;
113 }
114 }
115
116 let read_model: &Model = model;
123 let face_total = faces.len() as u64;
128 let faces_done = std::sync::atomic::AtomicU64::new(0);
129 type FaceWork = (Triangulation, Vec<(Shape, Vec<u32>)>);
130 let jobs: Vec<(&Shape, std::sync::Mutex<Option<OgeomResult<Triangulation>>>)> = faces
134 .iter()
135 .zip(meshes)
136 .map(|(face, mesh)| (face, std::sync::Mutex::new(Some(mesh))))
137 .collect();
138 let computed: Vec<OgeomResult<FaceWork>> =
139 ogeom_core::parallel::map_ordered(&jobs, |_, (face, slot)| {
140 ogeom_core::progress::checkpoint()?;
141 let face: &Shape = face;
142 let mesh = slot
143 .lock()
144 .ok()
145 .and_then(|mut held| held.take())
146 .unwrap_or_else(|| {
147 Err(ogeom_core::ogeom_err!(
148 Construction,
149 "a face's mesh was taken twice"
150 ))
151 })?;
152 ogeom_core::progress::stage_at(
153 "tessellate: faces",
154 faces_done.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1,
155 face_total,
156 );
157
158 let mut paths: Vec<(Shape, Vec<u32>)> = Vec::new();
162 let mut seen: Vec<(ogeom_topo::TShapeId, ogeom_topo::Location)> = Vec::new();
163 let index = MeshIndex::of(&mesh);
164 for edge in ogeom_topo::explore(read_model, face, Filter::OfType(ShapeType::Edge))? {
165 let key = (edge.node(), edge.location().clone());
166 if seen.contains(&key) {
167 continue;
168 }
169 seen.push(key);
170 let Some(line) = lines.get(&edge.node().index()) else {
172 continue;
173 };
174 if line.points.len() < 2 {
175 continue;
176 }
177 let placement = edge.transform(read_model.datums())?;
178 let mut points: Vec<ogeom_math::Point> =
179 line.points.iter().map(|p| placement.apply(*p)).collect();
180 if edge.orientation() == ogeom_topo::Orientation::Reversed {
181 points.reverse();
182 }
183 if let Some(indices) =
184 index.path(&mesh, &points, edge_reach(read_model, &edge, tol))
185 {
186 paths.push((edge, indices));
187 }
188 }
189 Ok((mesh, paths))
190 });
191
192 for (face, work) in faces.iter().zip(computed) {
193 let face = face.clone();
194 let (mesh, paths) = work?;
195 done.triangles += mesh.triangle_count();
196 done.deflection_met &= mesh.deflection_met;
197
198 let id = model.geometry_mut().add_triangulation(mesh);
199 for (edge, indices) in paths {
200 let Some(node) = model.node_mut(&edge) else {
201 continue;
202 };
203 let NodeData::Edge(data) = node.data_mut() else {
204 continue;
205 };
206 data.representations.push(EdgeRepr::PolygonOnTriangulation {
207 triangulation: id,
208 indices,
209 location: edge.location().clone(),
210 });
211 }
212
213 let Some(node) = model.node_mut(&face) else {
214 ogeom_bail!(Dangling, "face is not in this model");
215 };
216 let NodeData::Face(data) = node.data_mut() else {
217 ogeom_bail!(Construction, "face node holds no face data");
218 };
219 data.triangulation = Some(id);
220 done.faces += 1;
221 }
222 Ok(done)
223}
224
225fn edge_reach(model: &Model, edge: &Shape, tol: Tolerances) -> f64 {
228 let recorded = model
229 .node(edge)
230 .and_then(|n| n.data().as_edge())
231 .map_or(0.0, |d| d.tolerance.get());
232 recorded.max(tol.confusion() * 1e3)
233}
234
235struct MeshIndex {
238 by_bits: ogeom_core::FastMap<[u64; 3], Vec<u32>>,
241 adjacent: ogeom_core::FastSet<(u32, u32)>,
243}
244
245impl MeshIndex {
246 fn of(mesh: &Triangulation) -> Self {
247 let mut by_bits: ogeom_core::FastMap<[u64; 3], Vec<u32>> = ogeom_core::FastMap::default();
248 by_bits.reserve(mesh.positions.len());
249 for (i, p) in mesh.positions.iter().enumerate() {
250 #[allow(clippy::cast_possible_truncation)]
251 by_bits.entry(bits(*p)).or_default().push(i as u32);
252 }
253 let mut adjacent: ogeom_core::FastSet<(u32, u32)> = ogeom_core::FastSet::default();
254 adjacent.reserve(mesh.triangles.len() * 3 / 2);
255 for t in &mesh.triangles {
256 for i in 0..3 {
257 let (a, b) = (t[i], t[(i + 1) % 3]);
258 adjacent.insert((a.min(b), a.max(b)));
259 }
260 }
261 Self { by_bits, adjacent }
262 }
263
264 fn candidates<'a>(
267 &'a self,
268 mesh: &Triangulation,
269 p: ogeom_math::Point,
270 reach: f64,
271 ) -> std::borrow::Cow<'a, [u32]> {
272 if let Some(exact) = self.by_bits.get(&bits(p)) {
273 return std::borrow::Cow::Borrowed(exact);
274 }
275 let mut near: Vec<(f64, u32)> = Vec::new();
276 for (i, q) in mesh.positions.iter().enumerate() {
277 let d = q.distance(p);
278 if d <= reach {
279 #[allow(clippy::cast_possible_truncation)]
280 near.push((d, i as u32));
281 }
282 }
283 near.sort_by(|a, b| a.0.total_cmp(&b.0));
284 std::borrow::Cow::Owned(near.into_iter().map(|(_, i)| i).collect())
285 }
286
287 fn path(
297 &self,
298 mesh: &Triangulation,
299 points: &[ogeom_math::Point],
300 reach: f64,
301 ) -> Option<Vec<u32>> {
302 let walk = |start: u32| -> Option<Vec<u32>> {
303 let mut out = Vec::with_capacity(points.len());
304 out.push(start);
305 for p in &points[1..] {
306 let previous = *out.last()?;
307 let next = self
308 .candidates(mesh, *p, reach)
309 .iter()
310 .copied()
311 .find(|&c| self.adjacent.contains(&(previous.min(c), previous.max(c))))?;
312 out.push(next);
313 }
314 Some(out)
315 };
316 self.candidates(mesh, *points.first()?, reach)
317 .iter()
318 .copied()
319 .find_map(walk)
320 }
321}
322
323fn bits(p: ogeom_math::Point) -> [u64; 3] {
325 [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()]
326}
327
328#[must_use]
330pub fn triangulation_of<'a>(model: &'a Model, face: &Shape) -> Option<&'a Triangulation> {
331 let NodeData::Face(data) = model.node(face)?.data() else {
332 return None;
333 };
334 model.geometry().triangulation(data.triangulation?)
335}
336
337#[must_use]
339pub fn polyline_of(model: &Model, edge: &Shape) -> Option<(Vec<ogeom_math::Point>, Vec<f64>)> {
340 let NodeData::Edge(data) = model.node(edge)?.data() else {
341 return None;
342 };
343 data.representations.iter().find_map(|repr| match repr {
344 EdgeRepr::Polyline {
345 points, parameters, ..
346 } => Some((points.clone(), parameters.clone())),
347 _ => None,
348 })
349}
350
351fn edge_line(
356 model: &Model,
357 edge: &Shape,
358 deflection: Deflection,
359 tol: Tolerances,
360) -> OgeomResult<Option<Polyline>> {
361 let Some(node) = model.node(edge) else {
362 ogeom_bail!(Dangling, "edge is not in this model");
363 };
364 let NodeData::Edge(data) = node.data() else {
365 ogeom_bail!(Construction, "edge node holds no edge data");
366 };
367 crate::triangulate::edge_polyline(model, data, deflection, tol)
368}
369
370fn store_polyline(
373 model: &mut Model,
374 edge: &Shape,
375 line: Polyline,
376 deflection: Deflection,
377) -> OgeomResult<()> {
378 let Some(node) = model.node_mut(edge) else {
379 ogeom_bail!(Dangling, "edge is not in this model");
380 };
381 let NodeData::Edge(data) = node.data_mut() else {
382 ogeom_bail!(Construction, "edge node holds no edge data");
383 };
384 data.representations.retain(|repr| {
385 !matches!(
386 repr,
387 EdgeRepr::Polyline { .. } | EdgeRepr::PolygonOnTriangulation { .. }
388 )
389 });
390 data.add(EdgeRepr::Polyline {
391 points: line.points,
392 parameters: line.parameters,
393 location: ogeom_topo::Location::identity(),
394 deflection: deflection.chord,
395 });
396 Ok(())
397}
398
399#[cfg(test)]
400#[allow(clippy::unwrap_used, clippy::expect_used)]
401mod tests {
402 use super::*;
403 use ogeom_algo::make_box;
404 use ogeom_math::Frame;
405
406 const T: Tolerances = Tolerances::millimetres();
407
408 fn fine() -> Deflection {
409 Deflection {
410 chord: 1e-3,
411 angular: 0.05,
412 ..Deflection::default()
413 }
414 }
415
416 #[test]
417 fn tessellating_a_box_stores_a_mesh_on_every_face_and_edge() {
418 let mut model = Model::new();
419 let built = make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T).unwrap();
420
421 let done = tessellate(&mut model, &built.shape, fine(), T).unwrap();
422 assert_eq!(done.faces, 6);
423 assert_eq!(done.edges, 12);
424 assert_eq!(done.triangles, 12);
425 assert!(done.deflection_met);
426
427 for face in explore_unique(&model, &built.shape, ShapeType::Face).unwrap() {
428 let mesh = triangulation_of(&model, &face).expect("face has no triangulation");
429 assert_eq!(mesh.triangle_count(), 2);
430 }
431 for edge in explore_unique(&model, &built.shape, ShapeType::Edge).unwrap() {
432 let (points, parameters) = polyline_of(&model, &edge).expect("edge has no polyline");
433 assert_eq!(points.len(), parameters.len());
434 assert_eq!(points.len(), 2, "a straight edge is its own polyline");
435 }
436 }
437
438 #[test]
439 fn the_cached_boundary_agrees_with_the_cached_faces() {
440 let mut model = Model::new();
445 let built = make_box(&mut model, Frame::WORLD, (1.0, 2.0, 3.0), T).unwrap();
446 tessellate(&mut model, &built.shape, fine(), T).unwrap();
447
448 for edge in explore_unique(&model, &built.shape, ShapeType::Edge).unwrap() {
449 let (points, _) = polyline_of(&model, &edge).unwrap();
450 for face in
451 ogeom_topo::ancestors_of(&model, &built.shape, &edge, ShapeType::Face).unwrap()
452 {
453 let mesh = triangulation_of(&model, &face).unwrap();
454 for p in &points {
455 assert!(
456 mesh.positions.iter().any(|q| q.is_equal(*p, T)),
457 "the face's mesh has no vertex at {p:?}, which its edge's \
458 polyline passes through"
459 );
460 }
461 }
462 }
463 }
464
465 #[test]
466 fn a_narrow_face_draws_its_edges_finer_and_its_neighbours_agree() {
467 let mut model = Model::new();
474 let coarse = Deflection {
475 chord: 1.0,
476 ..Deflection::default()
477 };
478 let disc = ogeom_algo::make_cylinder(&mut model, Frame::WORLD, 10.0, 0.5, T).unwrap();
479 tessellate(&mut model, &disc.shape, coarse, T).unwrap();
480
481 let mut refined = 0;
482 for edge in explore_unique(&model, &disc.shape, ShapeType::Edge).unwrap() {
483 let (points, _) = polyline_of(&model, &edge).unwrap();
484 let alone = crate::triangulate::polyline_of_edge(&model, &edge, coarse, T).unwrap();
485 if points.len() > alone.len() {
486 refined += 1;
487 }
488 for face in
489 ogeom_topo::ancestors_of(&model, &disc.shape, &edge, ShapeType::Face).unwrap()
490 {
491 let mesh = triangulation_of(&model, &face).unwrap();
492 for p in &points {
493 assert!(
494 mesh.positions.iter().any(|q| q.is_equal(*p, T)),
495 "the face's mesh has no vertex at {p:?}, which its edge's \
496 polyline passes through"
497 );
498 }
499 }
500 }
501 assert!(
502 refined >= 2,
503 "the rim's circles are drawn finer than the coarse chord asks"
504 );
505 }
506
507 #[test]
508 fn tessellating_again_replaces_rather_than_accumulates() {
509 let mut model = Model::new();
512 let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
513
514 tessellate(&mut model, &built.shape, Deflection::default(), T).unwrap();
515 tessellate(&mut model, &built.shape, fine(), T).unwrap();
516
517 for edge in explore_unique(&model, &built.shape, ShapeType::Edge).unwrap() {
518 let NodeData::Edge(data) = model.node(&edge).unwrap().data() else {
519 unreachable!()
520 };
521 let polylines = data
522 .representations
523 .iter()
524 .filter(|r| matches!(r, EdgeRepr::Polyline { .. }))
525 .count();
526 assert_eq!(polylines, 1, "the earlier polyline was left behind");
527 }
528 }
529
530 #[test]
531 fn a_shape_with_no_faces_tessellates_to_nothing_rather_than_failing() {
532 let mut model = Model::new();
533 let vertex = model.add_point(ogeom_math::Point::ORIGIN);
534 let done = tessellate(&mut model, &vertex, fine(), T).unwrap();
535 assert_eq!(done.faces, 0);
536 assert_eq!(done.edges, 0);
537 assert_eq!(done.triangles, 0);
538 assert!(done.deflection_met);
539 assert!(triangulation_of(&model, &vertex).is_none());
540 assert!(polyline_of(&model, &vertex).is_none());
541 }
542
543 #[test]
544 fn an_unusable_deflection_is_refused_before_anything_is_stored() {
545 let mut model = Model::new();
546 let built = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
547 let bad = Deflection {
548 chord: 0.0,
549 ..Deflection::default()
550 };
551 assert!(tessellate(&mut model, &built.shape, bad, T).is_err());
552
553 let face = explore_unique(&model, &built.shape, ShapeType::Face).unwrap()[0].clone();
554 assert!(triangulation_of(&model, &face).is_none());
555 }
556}
557#[cfg(test)]
558#[allow(clippy::unwrap_used)]
559mod polygon_on_tests {
560 use super::*;
561 use ogeom_core::Tolerances;
562 use ogeom_math::Frame;
563 use ogeom_topo::{EdgeRepr, Filter, ShapeType, explore};
564
565 const T: Tolerances = Tolerances::millimetres();
566
567 fn fine() -> Deflection {
568 Deflection {
569 chord: 1e-2,
570 ..Deflection::default()
571 }
572 }
573
574 #[test]
575 fn every_edge_walks_its_faces_triangulations_by_index() {
576 let mut model = Model::new();
577 let solid = ogeom_algo::make_cylinder(&mut model, Frame::WORLD, 2.0, 5.0, T).unwrap();
578 tessellate(&mut model, &solid.shape, fine(), T).unwrap();
579
580 let mut checked = 0;
581 for face in explore(&model, &solid.shape, Filter::OfType(ShapeType::Face)).unwrap() {
582 let mesh_id = {
583 let ogeom_topo::NodeData::Face(data) = model.node(&face).unwrap().data() else {
584 panic!("face data");
585 };
586 data.triangulation.unwrap()
587 };
588 let mesh = model.geometry().triangulation(mesh_id).unwrap();
589 let mut edges_of = ogeom_core::FastSet::default();
591 for t in &mesh.triangles {
592 for i in 0..3 {
593 let (a, b) = (t[i], t[(i + 1) % 3]);
594 edges_of.insert((a.min(b), a.max(b)));
595 }
596 }
597 for edge in explore(&model, &face, Filter::OfType(ShapeType::Edge)).unwrap() {
598 let data = model.node(&edge).unwrap().data().as_edge().unwrap();
599 if data.degenerate {
600 continue;
601 }
602 let paths: Vec<&Vec<u32>> = data
603 .representations
604 .iter()
605 .filter_map(|r| match r {
606 EdgeRepr::PolygonOnTriangulation {
607 triangulation,
608 indices,
609 ..
610 } if *triangulation == mesh_id => Some(indices),
611 _ => None,
612 })
613 .collect();
614 assert!(
615 !paths.is_empty(),
616 "an edge of a tessellated face walks its triangulation"
617 );
618 for indices in paths {
619 assert!(indices.len() >= 2);
620 for pair in indices.windows(2) {
621 let key = (pair[0].min(pair[1]), pair[0].max(pair[1]));
622 assert!(
623 edges_of.contains(&key),
624 "consecutive indices are a triangle edge of the mesh: \
625 {:?} at {:?} and {:?}",
626 pair,
627 mesh.positions[pair[0] as usize],
628 mesh.positions[pair[1] as usize]
629 );
630 }
631 checked += 1;
632 }
633 }
634 }
635 assert!(checked >= 6, "rings, seam sides and rims all walked");
636 }
637
638 #[test]
639 fn each_path_runs_through_the_points_the_edge_draws_on_its_own() {
640 let mut model = Model::new();
646 let solid = ogeom_algo::make_cylinder(&mut model, Frame::WORLD, 2.0, 5.0, T).unwrap();
647 tessellate(&mut model, &solid.shape, fine(), T).unwrap();
648
649 let mut compared = 0;
650 for face in explore(&model, &solid.shape, Filter::OfType(ShapeType::Face)).unwrap() {
651 let mesh_id = model
652 .node(&face)
653 .unwrap()
654 .data()
655 .as_face()
656 .unwrap()
657 .triangulation;
658 let mesh = model.geometry().triangulation(mesh_id.unwrap()).unwrap();
659 for edge in explore(&model, &face, Filter::OfType(ShapeType::Edge)).unwrap() {
660 let alone = crate::triangulate::polyline_of_edge(&model, &edge, fine(), T).unwrap();
661 let data = model.node(&edge).unwrap().data().as_edge().unwrap();
662 for repr in &data.representations {
663 let EdgeRepr::PolygonOnTriangulation {
664 triangulation,
665 indices,
666 ..
667 } = repr
668 else {
669 continue;
670 };
671 if Some(*triangulation) != mesh_id {
672 continue;
673 }
674 let walked: Vec<_> = indices
675 .iter()
676 .map(|&i| mesh.positions[i as usize])
677 .collect();
678 let forward = walked == alone;
679 let backward = walked.iter().rev().eq(alone.iter());
680 assert!(forward || backward, "a stored path left its edge's points");
681 compared += 1;
682 }
683 }
684 }
685 assert!(compared >= 6, "every face's edges compared");
686 }
687}