1mod clip;
14mod grid;
15pub mod polygon_triangulate;
16pub mod stl;
17mod topology_debug;
18
19use std::collections::HashMap;
20
21use geop_core_math::{
22 geop_error::{GeopError, GeopResult},
23 primitives::{Color10, PrimitiveScene, TriangleFace},
24 scalars::Scalar,
25 vector::{Vector2, Vector3},
26};
27use geop_core_topology::{EdgeId, Face, FaceId, Model, VertexId};
28
29pub use topology_debug::rasterize_topology;
30
31pub fn face_triangles_uv<S: Scalar>(
38 model: &Model<S>,
39 face: &Face<S>,
40 n: usize,
41) -> GeopResult<Vec<(Vector2<S>, Vector2<S>, Vector2<S>)>> {
42 grid::triangulate_face(model, face, n)
43}
44
45pub struct RasterizedModel<S: Scalar> {
55 pub vertices: HashMap<VertexId, Vector3<S>>,
56 pub edges: HashMap<EdgeId, Vec<Vector3<S>>>,
58 pub faces: HashMap<FaceId, Vec<TriangleFace<S>>>,
62}
63
64const MIN_EDGE_SEGMENTS: usize = 1;
66const MAX_EDGE_DOUBLINGS: u32 = 10;
68
69fn sample_curve<S: Scalar>(
75 curve: &geop_core_geometry::nurb_curve::NurbCurve3D<S>,
76 quality: usize,
77) -> GeopResult<Vec<Vector3<S>>> {
78 let (t0, t1) = curve.domain();
79 let at = |frac: (usize, usize)| -> GeopResult<Vector3<S>> {
80 let f = S::from_ratio(frac.0 as i64, frac.1 as i64)?;
81 curve.evaluate(t0.add(t1.sub(t0).mul(f)))
82 };
83 let points_at = |segments: usize| -> GeopResult<Vec<Vector3<S>>> {
84 (0..=segments).map(|i| at((i, segments))).collect()
85 };
86
87 let mut segments = MIN_EDGE_SEGMENTS;
88 let mut points = points_at(segments)?;
89 let f64_pt = |p: &Vector3<S>| [p[0].to_f64(), p[1].to_f64(), p[2].to_f64()];
90 let size = points
91 .iter()
92 .flat_map(|p| points.iter().map(move |q| dist3(f64_pt(p), f64_pt(q))))
93 .fold(0.0f64, f64::max)
94 .max(1e-9);
95 let tolerance = size / (4.0 * (quality * quality) as f64);
96
97 for _ in 0..MAX_EDGE_DOUBLINGS {
98 let mut worst = 0.0f64;
100 for i in 0..segments {
101 let mid = at((2 * i + 1, 2 * segments))?;
102 worst = worst.max(dist_point_to_segment(
103 f64_pt(&mid),
104 f64_pt(&points[i]),
105 f64_pt(&points[i + 1]),
106 ));
107 }
108 if worst <= tolerance {
109 break;
110 }
111 segments *= 2;
112 points = points_at(segments)?;
113 }
114 Ok(points)
115}
116
117fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
118 let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
119 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
120}
121
122fn dist_point_to_segment(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f64 {
124 let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
125 let len2 = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2];
126 let t = if len2 > 1e-18 {
127 (((p[0] - a[0]) * ab[0] + (p[1] - a[1]) * ab[1] + (p[2] - a[2]) * ab[2]) / len2)
128 .clamp(0.0, 1.0)
129 } else {
130 0.0
131 };
132 dist3(p, [a[0] + ab[0] * t, a[1] + ab[1] * t, a[2] + ab[2] * t])
133}
134
135pub fn rasterize_model_tagged<S: Scalar>(
140 model: &Model<S>,
141 n: usize,
142) -> GeopResult<RasterizedModel<S>> {
143 if n < 2 {
144 return Err(GeopError::new("rasterize_model_tagged: n must be >= 2"));
145 }
146
147 let vertices = model
148 .vertices
149 .iter()
150 .map(|(&id, vertex)| (id, vertex.point))
151 .collect();
152
153 let mut edges = HashMap::with_capacity(model.edges.len());
154 for (&id, edge) in model.edges.iter() {
155 edges.insert(id, sample_curve(&edge.curve, n)?);
156 }
157
158 let mut faces = HashMap::with_capacity(model.faces.len());
159 for (&id, face) in model.faces.iter() {
160 let mut tris = Vec::new();
161 let mut normals: HashMap<[u64; 2], Option<Vector3<S>>> = HashMap::new();
164 for (uv_a, uv_b, uv_c) in face_triangles_uv(model, face, n)? {
165 let a = face.surface.evaluate(uv_a[0], uv_a[1])?;
166 let b = face.surface.evaluate(uv_b[0], uv_b[1])?;
167 let c = face.surface.evaluate(uv_c[0], uv_c[1])?;
168 let Ok(t) = TriangleFace::try_new(a, b, c) else {
169 continue;
170 };
171 let corner = |uv: Vector2<S>, cache: &mut HashMap<[u64; 2], Option<Vector3<S>>>| {
175 let key = [uv[0].to_f64().to_bits(), uv[1].to_f64().to_bits()];
176 *cache
177 .entry(key)
178 .or_insert_with(|| face.surface.normal(uv[0], uv[1]).ok())
179 };
180 let (na, nb, nc) = (
181 corner(uv_a, &mut normals),
182 corner(uv_b, &mut normals),
183 corner(uv_c, &mut normals),
184 );
185 tris.push(match (na, nb, nc) {
186 (Some(na), Some(nb), Some(nc)) => t.with_vertex_normals([na, nb, nc]),
187 _ => t,
188 });
189 }
190 faces.insert(id, tris);
191 }
192
193 Ok(RasterizedModel {
194 vertices,
195 edges,
196 faces,
197 })
198}
199
200pub fn rasterize_model<S: Scalar>(model: &Model<S>, n: usize) -> GeopResult<PrimitiveScene<S>> {
205 rasterize_model_impl(model, n, false, &default_face_color)
206}
207
208pub fn rasterize_model_wireframe<S: Scalar>(
213 model: &Model<S>,
214 n: usize,
215) -> GeopResult<PrimitiveScene<S>> {
216 rasterize_model_impl(model, n, true, &default_face_color)
217}
218
219pub fn rasterize_model_with_face_color<S: Scalar>(
224 model: &Model<S>,
225 n: usize,
226 face_color: impl Fn(FaceId) -> Color10,
227) -> GeopResult<PrimitiveScene<S>> {
228 rasterize_model_impl(model, n, false, &move |id, _face| face_color(id))
229}
230
231fn default_face_color<S: Scalar>(_id: FaceId, _face: &Face<S>) -> Color10 {
232 Color10::Blue
233}
234
235fn rasterize_model_impl<S: Scalar>(
236 model: &Model<S>,
237 n: usize,
238 wireframe: bool,
239 face_color: &dyn Fn(FaceId, &Face<S>) -> Color10,
240) -> GeopResult<PrimitiveScene<S>> {
241 let rasterized = rasterize_model_tagged(model, n)?;
242 let mut scene = PrimitiveScene::new();
243
244 for point in rasterized.vertices.into_values() {
245 scene.add_point(point, Color10::DarkGray);
246 }
247
248 for polyline in rasterized.edges.into_values() {
249 scene.add_polyline(&polyline, Color10::Gray);
250 }
251
252 for (face_id, tris) in rasterized.faces {
253 let face = model.get_face(face_id)?;
254 let color = face_color(face_id, face);
255 for t in tris {
256 if wireframe {
257 for (p, q) in [(t.a, t.b), (t.b, t.c), (t.c, t.a)] {
258 if let Ok(l) = geop_core_math::primitives::Line::try_new(p, q) {
259 scene.add_line(l, color);
260 }
261 }
262 } else {
263 scene.add_triangle(t, color);
264 }
265 }
266 }
267
268 Ok(scene)
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use geop_core_math::for_all_scalars;
275 use geop_core_math::primitives::TriangleFace;
276 use geop_core_part::Part;
277 use geop_ops_extrude_revolve::{cube_solid, sphere::sphere_solid};
278
279 fn rasterize_and_save<S: Scalar>(model: &Model<S>, name: &str) {
282 let scene = rasterize_model(model, 8).unwrap();
283 assert!(!scene.points.is_empty());
284 assert!(!scene.lines.is_empty());
285 assert!(!scene.triangles.is_empty());
286 std::fs::create_dir_all("outputs").unwrap();
287 scene.save_to_file(&format!("outputs/{name}.html")).unwrap();
288 }
289
290 fn check_rasterize_cube<S: Scalar>() {
291 let mut part = Part::<S>::new();
292 cube_solid(
293 &mut part,
294 "t1",
295 Vector3::from_array([S::ZERO; 3]),
296 Vector3::from_array([S::ONE; 3]),
297 )
298 .unwrap();
299 let model = part.topology();
300 rasterize_and_save(&model, "cube");
301 }
302 #[test]
303 fn rasterize_cube() {
304 for_all_scalars!(check_rasterize_cube);
305 }
306
307 fn check_sphere_triangles_carry_surface_normals<S: Scalar>() {
315 let mut part = Part::<S>::new();
316 sphere_solid(&mut part, "t3", Vector3::zero(), S::ONE).unwrap();
317 let model = part.topology();
318 let scene = rasterize_model(&model, 24).unwrap();
319 assert!(!scene.triangles.is_empty());
320 let mut with_normals = 0;
321 for (t, _) in &scene.triangles {
322 let Some(normals) = t.vertex_normals else {
323 continue;
324 };
325 with_normals += 1;
326 for (n, p) in normals.iter().zip([t.a, t.b, t.c]) {
327 let dot = n.prod_dot(&p).to_f64().abs();
329 assert!(
330 (dot - 1.0).abs() < 1e-6,
331 "normal {n:?} is not the sphere's own at {p:?} (|n·p| = {dot})"
332 );
333 }
334 }
335 assert!(
336 with_normals * 20 > scene.triangles.len(),
337 "only {with_normals} of {} triangles carry surface normals",
338 scene.triangles.len()
339 );
340 }
341 #[test]
342 fn sphere_triangles_carry_surface_normals() {
343 for_all_scalars!(check_sphere_triangles_carry_surface_normals);
344 }
345
346 fn check_cylinder_wall_is_refined_only_around<S: Scalar>() {
352 let mut part = Part::<S>::new();
353 geop_ops_extrude_revolve::cylinder::revolved_cylinder(
354 &mut part,
355 "t5",
356 Vector3::zero(),
357 S::ONE,
358 S::from_f64(4.0),
359 )
360 .unwrap();
361 let model = part.topology();
362 let rasterized = rasterize_model_tagged(&model, 24).unwrap();
363 let walls: Vec<&Vec<TriangleFace<S>>> = rasterized
365 .faces
366 .values()
367 .filter(|tris| {
368 !tris.is_empty()
369 && tris.iter().all(|t| {
370 [t.a, t.b, t.c]
371 .iter()
372 .all(|p| p[0].mul(p[0]).add(p[1].mul(p[1])).could_be_equal(S::ONE))
373 })
374 })
375 .collect();
376 assert_eq!(walls.len(), 4, "a revolved cylinder has 4 wall quadrants");
377 for tris in walls {
378 assert!(
381 tris.len() <= 2 * 2 * 32,
382 "a cylinder wall quadrant came out as {} triangles",
383 tris.len()
384 );
385 assert!(tris.len() >= 2 * 2 * 8, "and it still has to look round");
386 }
387 }
388 #[test]
389 fn cylinder_wall_is_refined_only_around() {
390 for_all_scalars!(check_cylinder_wall_is_refined_only_around);
391 }
392
393 fn check_edge_sampling_follows_curvature<S: Scalar>() {
396 let mut part = Part::<S>::new();
397 cube_solid(
398 &mut part,
399 "t2",
400 Vector3::zero(),
401 Vector3::from_array([S::ONE; 3]),
402 )
403 .unwrap();
404 let cube_edges = rasterize_model_tagged(part.topology(), 24).unwrap().edges;
405 for polyline in cube_edges.values() {
406 assert_eq!(polyline.len(), 2, "a straight edge is a single segment");
407 }
408
409 let mut part = Part::<S>::new();
410 sphere_solid(&mut part, "t4", Vector3::zero(), S::ONE).unwrap();
411 let model = part.topology();
412 let sphere_edges = rasterize_model_tagged(&model, 24).unwrap().edges;
413 for polyline in sphere_edges.values() {
414 assert!(
417 polyline.len() >= 17,
418 "a quarter circle came out as {} points",
419 polyline.len()
420 );
421 for p in polyline {
422 let r = p[0].mul(p[0]).add(p[1].mul(p[1])).add(p[2].mul(p[2]));
423 assert!(r.could_be_equal(S::ONE), "sample off the sphere: {p:?}");
424 }
425 }
426 }
427 #[test]
428 fn edge_sampling_follows_curvature() {
429 for_all_scalars!(check_edge_sampling_follows_curvature);
430 }
431}