brep_kernel/geometry/analytic_surface/
recognition.rs1use super::*;
2
3pub(super) fn nearly_equal_points(a: &NurbsSurface, b: &NurbsSurface, scale: f64) -> bool {
4 if a.degree_u != b.degree_u
5 || a.degree_v != b.degree_v
6 || a.knots_u.len() != b.knots_u.len()
7 || a.knots_v.len() != b.knots_v.len()
8 || a.control_points.len() != b.control_points.len()
9 {
10 return false;
11 }
12 let knot_tolerance = crate::curve::KNOT_DEDUP_EPS;
14 if a.knots_u
15 .iter()
16 .zip(&b.knots_u)
17 .any(|(x, y)| (x - y).abs() > knot_tolerance)
18 || a.knots_v
19 .iter()
20 .zip(&b.knots_v)
21 .any(|(x, y)| (x - y).abs() > knot_tolerance)
22 {
23 return false;
24 }
25 let tolerance = RECOGNITION_TOLERANCE * scale.max(1.0);
26 a.control_points
27 .iter()
28 .zip(&b.control_points)
29 .all(|(row_a, row_b)| {
30 row_a.len() == row_b.len()
31 && row_a.iter().zip(row_b).all(|(p, q)| {
32 (p.x - q.x).abs() <= tolerance
33 && (p.y - q.y).abs() <= tolerance
34 && (p.z - q.z).abs() <= tolerance
35 && (p.w - q.w).abs() <= RECOGNITION_TOLERANCE
36 })
37 })
38}
39
40pub(super) fn surface_scale(surface: &NurbsSurface) -> f64 {
41 surface
42 .control_points
43 .iter()
44 .flatten()
45 .map(|p| (p.x.abs() / p.w).max(p.y.abs() / p.w).max(p.z.abs() / p.w))
46 .fold(0.0, f64::max)
47}
48
49pub(super) fn full_circle_knots(spans: usize) -> Vec<f64> {
50 let mut knots = vec![0.0, 0.0, 0.0];
51 for index in 1..spans {
52 let knot = index as f64 / spans as f64;
53 knots.extend([knot, knot]);
54 }
55 knots.extend([1.0, 1.0, 1.0]);
56 knots
57}
58
59fn recognize_plane(surface: &NurbsSurface) -> Option<AnalyticSurface> {
60 if !surface.is_affine().unwrap_or(false) {
61 return None;
62 }
63 let u_domain = [surface.knots_u[0], *surface.knots_u.last()?];
64 let v_domain = [surface.knots_v[0], *surface.knots_v.last()?];
65 let extent_u = u_domain[1] - u_domain[0];
66 let extent_v = v_domain[1] - v_domain[0];
67 if extent_u <= 0.0 || extent_v <= 0.0 {
68 return None;
69 }
70 let p00 = surface.control_points[0][0].point().ok()?;
71 let p10 = surface.control_points[1][0].point().ok()?;
72 let p01 = surface.control_points[0][1].point().ok()?;
73 Some(AnalyticSurface::Plane {
74 origin: p00,
75 u_dir: p10.sub(p00).scale(1.0 / extent_u),
76 v_dir: p01.sub(p00).scale(1.0 / extent_v),
77 u_domain,
78 v_domain,
79 })
80}
81
82fn recognize_revolution(surface: &NurbsSurface) -> Option<AnalyticSurface> {
83 if surface.degree_u != 2 || surface.control_points.len() != 9 {
84 return None;
85 }
86 let expected_knots = full_circle_knots(4);
87 if surface.knots_u.len() != expected_knots.len()
88 || surface
89 .knots_u
90 .iter()
91 .zip(&expected_knots)
92 .any(|(a, b)| (a - b).abs() > 1e-12)
93 {
94 return None;
95 }
96 let scale = surface_scale(surface);
97 let rows = &surface.control_points;
98 let mut frame: Option<RevolutionFrame> = None;
101 for column in 0..rows[0].len() {
102 let p0 = rows[0][column].point().ok()?;
103 let p180 = rows[4][column].point().ok()?;
104 let p90 = rows[2][column].point().ok()?;
105 let center = p0.add(p180).scale(0.5);
106 let radial = p0.sub(center);
107 let radius = radial.length();
108 if radius <= RECOGNITION_TOLERANCE * scale.max(1.0) {
109 continue;
110 }
111 let toward_90 = p90.sub(center);
112 let axis = radial.cross(toward_90).normalized().ok()?;
113 let x_axis = radial.scale(1.0 / radius);
114 let y_axis = axis.cross(x_axis);
115 frame = Some(RevolutionFrame {
116 origin: center,
117 axis,
118 x_axis,
119 y_axis,
120 });
121 break;
122 }
123 let frame = frame?;
124 let generatrix =
125 NurbsCurve::new(surface.degree_v, surface.knots_v.clone(), rows[0].clone()).ok()?;
126 let rebuilt =
127 make_revolution(frame.origin, frame.axis, &generatrix, std::f64::consts::TAU).ok()?;
128 if !nearly_equal_points(surface, &rebuilt, scale) {
129 return None;
130 }
131 classify_generatrix(surface, frame, &generatrix, scale)
132}
133
134fn classify_generatrix(
135 surface: &NurbsSurface,
136 frame: RevolutionFrame,
137 generatrix: &NurbsCurve,
138 scale: f64,
139) -> Option<AnalyticSurface> {
140 let tolerance = RECOGNITION_TOLERANCE * scale.max(1.0);
141 let controls = &generatrix.control_points;
142 if generatrix.degree == 1
143 && controls.len() == 2
144 && (controls[0].w - 1.0).abs() <= RECOGNITION_TOLERANCE
145 && (controls[1].w - 1.0).abs() <= RECOGNITION_TOLERANCE
146 && (surface.knots_v[0], *surface.knots_v.last()?) == (0.0, 1.0)
147 && generatrix_is_meridional_half_ray(generatrix, &frame)
148 {
149 let p0 = controls[0].point().ok()?;
150 let p1 = controls[1].point().ok()?;
151 let (_, rho0, z0) = frame.cylindrical(p0);
152 let (_, rho1, z1) = frame.cylindrical(p1);
153 let height = z1 - z0;
154 if height.abs() <= tolerance {
155 return None;
156 }
157 let origin = frame.origin.add(frame.axis.scale(z0));
160 return Some(AnalyticSurface::RuledRevolution {
161 frame: RevolutionFrame { origin, ..frame },
162 rho0,
163 rho1,
164 height,
165 });
166 }
167 if generatrix.degree == 2 && controls.len() == 5 {
168 let south = controls[0].point().ok()?;
170 let north = controls[4].point().ok()?;
171 let (_, rho_south, z_south) = frame.cylindrical(south);
172 let (_, rho_north, z_north) = frame.cylindrical(north);
173 if rho_south <= tolerance && rho_north <= tolerance {
174 let radius = (z_north - z_south) * 0.5;
175 if radius <= tolerance {
176 return None;
177 }
178 let center = frame.origin.add(frame.axis.scale(z_south + radius));
179 let meridian = make_arc(
180 center,
181 frame.x_axis,
182 frame.axis,
183 radius,
184 -std::f64::consts::FRAC_PI_2,
185 std::f64::consts::FRAC_PI_2,
186 )
187 .ok()?;
188 if curves_nearly_equal(generatrix, &meridian, scale) {
189 return Some(AnalyticSurface::Sphere {
190 frame: RevolutionFrame {
191 origin: center,
192 ..frame
193 },
194 radius,
195 });
196 }
197 }
198 return None;
199 }
200 if generatrix.degree == 2 && controls.len() == 9 {
201 let tube_start = controls[0].point().ok()?;
202 let tube_opposite = controls[4].point().ok()?;
203 let tube_center = tube_start.add(tube_opposite).scale(0.5);
204 let minor = tube_start.sub(tube_center).length();
205 let (_, major, axial) = frame.cylindrical(tube_center);
206 if minor <= tolerance || major <= minor || axial.abs() > tolerance {
207 return None;
208 }
209 let tube = make_arc(
210 tube_center,
211 frame.x_axis,
212 frame.axis,
213 minor,
214 0.0,
215 std::f64::consts::TAU,
216 )
217 .ok()?;
218 if curves_nearly_equal(generatrix, &tube, scale) {
219 return Some(AnalyticSurface::Torus {
220 frame,
221 major_radius: major,
222 minor_radius: minor,
223 });
224 }
225 return None;
226 }
227 None
228}
229
230fn curves_nearly_equal(a: &NurbsCurve, b: &NurbsCurve, scale: f64) -> bool {
231 if a.degree != b.degree
232 || a.knots.len() != b.knots.len()
233 || a.control_points.len() != b.control_points.len()
234 {
235 return false;
236 }
237 if a.knots
238 .iter()
239 .zip(&b.knots)
240 .any(|(x, y)| (x - y).abs() > 1e-12)
241 {
242 return false;
243 }
244 let tolerance = RECOGNITION_TOLERANCE * scale.max(1.0);
245 a.control_points
246 .iter()
247 .zip(&b.control_points)
248 .all(|(p, q)| {
249 (p.x - q.x).abs() <= tolerance
250 && (p.y - q.y).abs() <= tolerance
251 && (p.z - q.z).abs() <= tolerance
252 && (p.w - q.w).abs() <= RECOGNITION_TOLERANCE
253 })
254}
255
256pub fn recognize(surface: &NurbsSurface) -> Option<AnalyticSurface> {
257 recognize_plane(surface)
258 .or_else(|| recognize_revolution(surface))
259 .or_else(|| recognize_general_revolution(surface))
260}
261
262fn recognize_general_revolution(surface: &NurbsSurface) -> Option<AnalyticSurface> {
266 let structure = revolution_structure(surface)?;
267 let spans = (surface.control_points.len() - 1) / 2;
268 Some(AnalyticSurface::Revolution {
269 frame: structure.frame,
270 spans,
271 sweep: structure.sweep,
272 generatrix: structure.generatrix,
273 })
274}
275
276pub(super) fn generatrix_is_meridional_half_ray(
280 generatrix: &NurbsCurve,
281 frame: &RevolutionFrame,
282) -> bool {
283 let mut scale = 0.0_f64;
284 for control in &generatrix.control_points {
285 let Ok(point) = control.point() else {
286 return false;
287 };
288 let offset = point.sub(frame.origin);
289 let radial = offset.sub(frame.axis.scale(offset.dot(frame.axis)));
290 scale = scale.max(radial.length());
291 }
292 let tolerance = RECOGNITION_TOLERANCE * scale.max(1.0);
293 generatrix.control_points.iter().all(|control| {
294 let Ok(point) = control.point() else {
295 return false;
296 };
297 let offset = point.sub(frame.origin);
298 let radial = offset.sub(frame.axis.scale(offset.dot(frame.axis)));
299 radial.dot(frame.y_axis).abs() <= tolerance
300 && radial.dot(frame.x_axis) >= -tolerance
301 })
302}
303
304impl AnalyticSurface {
305 pub fn sphere_geometry(&self) -> Option<(Vec3, f64)> {
324 match self {
325 AnalyticSurface::Sphere { frame, radius } => Some((frame.origin, *radius)),
326 AnalyticSurface::Revolution {
327 frame,
328 sweep,
329 generatrix,
330 ..
331 } => {
332 if *sweep < std::f64::consts::TAU - 1e-9 {
333 return None;
334 }
335 let controls = &generatrix.control_points;
336 if generatrix.degree != 2 || controls.len() != 5 {
337 return None;
338 }
339 let start = controls[0].point().ok()?;
340 let end = controls[4].point().ok()?;
341 let scale = curve_scale(generatrix);
342 let tolerance = RECOGNITION_TOLERANCE * scale.max(1.0);
343 let (_, rho_start, z_start) = frame.cylindrical(start);
345 let (_, rho_end, z_end) = frame.cylindrical(end);
346 if rho_start > tolerance || rho_end > tolerance {
347 return None;
348 }
349 let radius = (z_end - z_start).abs() * 0.5;
350 if radius <= tolerance {
351 return None;
352 }
353 let center = start.add(end).scale(0.5);
358 let polar = if z_end > z_start {
360 frame.axis
361 } else {
362 frame.axis.scale(-1.0)
363 };
364 let meridian = make_arc(
365 center,
366 frame.x_axis,
367 polar,
368 radius,
369 -std::f64::consts::FRAC_PI_2,
370 std::f64::consts::FRAC_PI_2,
371 )
372 .ok()?;
373 curves_nearly_equal(generatrix, &meridian, scale).then_some((center, radius))
374 }
375 _ => None,
376 }
377 }
378
379 pub fn sphere_frame(&self) -> Option<(Vec3, f64, [Vec3; 3])> {
392 let (center, radius) = self.sphere_geometry()?;
393 let frame = match self {
394 AnalyticSurface::Sphere { frame, .. } => frame,
395 AnalyticSurface::Revolution { frame, .. } => frame,
396 _ => return None,
397 };
398 Some((center, radius, [frame.x_axis, frame.y_axis, frame.axis]))
399 }
400
401 pub fn torus_geometry(&self) -> Option<(RevolutionFrame, f64, f64)> {
408 match self {
409 AnalyticSurface::Torus {
410 frame,
411 major_radius,
412 minor_radius,
413 } => Some((frame.clone(), *major_radius, *minor_radius)),
414 AnalyticSurface::Revolution {
415 frame,
416 sweep,
417 generatrix,
418 ..
419 } => {
420 if *sweep < std::f64::consts::TAU - 1e-9 {
421 return None;
422 }
423 let controls = &generatrix.control_points;
424 if generatrix.degree != 2 || controls.len() != 9 {
425 return None;
426 }
427 let tube_start = controls[0].point().ok()?;
428 let tube_opposite = controls[4].point().ok()?;
429 let tube_center = tube_start.add(tube_opposite).scale(0.5);
430 let minor = tube_start.sub(tube_center).length();
431 let scale = curve_scale(generatrix);
432 let tolerance = RECOGNITION_TOLERANCE * scale.max(1.0);
433 let (_, major, axial) = frame.cylindrical(tube_center);
434 if minor <= tolerance || major <= minor {
435 return None;
436 }
437 for polar in [frame.axis, frame.axis.scale(-1.0)] {
438 let tube = make_arc(
439 tube_center,
440 frame.x_axis,
441 polar,
442 minor,
443 0.0,
444 std::f64::consts::TAU,
445 )
446 .ok()?;
447 if curves_nearly_equal(generatrix, &tube, scale) {
448 return Some((
449 RevolutionFrame {
450 origin: frame.origin.add(frame.axis.scale(axial)),
451 ..frame.clone()
452 },
453 major,
454 minor,
455 ));
456 }
457 }
458 None
459 }
460 _ => None,
461 }
462 }
463}
464
465fn curve_scale(curve: &NurbsCurve) -> f64 {
468 curve
469 .control_points
470 .iter()
471 .map(|p| (p.x.abs() / p.w).max(p.y.abs() / p.w).max(p.z.abs() / p.w))
472 .fold(0.0, f64::max)
473}