1use super::*;
2
3fn face_normal(face: &FaceRecord, u: f64, v: f64) -> Result<Vec3, String> {
4 let normal = match face.surface.normal(u, v) {
5 Ok(normal) => normal,
6 Err(error) => {
7 if !error.contains("zero-length")
17 || std::env::var("BREP_POLE_NORMAL_RESCUE").as_deref() == Ok("0")
18 {
19 return Err(error);
20 }
21 let [u0, u1] = face.surface.domain_u()?;
22 let [v0, v1] = face.surface.domain_v()?;
23 let step_u = (u1 - u0) * 1e-4;
24 let step_v = (v1 - v0) * 1e-4;
25 let inner_u = u.clamp(u0 + step_u, u1 - step_u);
26 let inner_v = v.clamp(v0 + step_v, v1 - step_v);
27 let mut recovered = None;
28 for (cu, cv) in [(u, inner_v), (inner_u, v), (inner_u, inner_v)] {
29 if let Ok(normal) = face.surface.normal(cu, cv) {
30 recovered = Some(normal);
31 break;
32 }
33 }
34 match recovered {
35 Some(normal) => normal,
36 None => return Err(error),
37 }
38 }
39 };
40 Ok(if face.same_sense {
41 normal
42 } else {
43 normal.scale(-1.0)
44 })
45}
46
47fn face_uv_tolerance(face: &FaceRecord, u: f64, v: f64, spatial: f64) -> f64 {
53 surface_uv_band(&face.surface, u, v, spatial)
54}
55
56pub(crate) fn surface_uv_band(surface: &crate::NurbsSurface, u: f64, v: f64, spatial: f64) -> f64 {
58 let Ok(derivatives) = surface.derivatives(u, v, 1) else {
59 return spatial;
60 };
61 let band = crate::tolerance::surface_uv_tolerance(
62 spatial,
63 derivatives[1][0].length(),
64 derivatives[0][1].length(),
65 );
66 let cap = match (surface.domain_u(), surface.domain_v()) {
67 (Ok([u0, u1]), Ok([v0, v1])) => ((u1 - u0).min(v1 - v0) * 0.05).max(1e-12),
68 _ => f64::INFINITY,
69 };
70 band.min(cap)
71}
72
73#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
74#[serde(rename_all = "lowercase")]
75pub enum PointClass {
76 In,
77 Out,
78 On,
79}
80
81#[derive(Clone, Copy, Debug, Serialize)]
82pub struct PointClassification {
83 pub class: PointClass,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub on_normal: Option<Vec3>,
86}
87
88#[derive(Default, Clone, Copy)]
95pub(crate) struct ClassifyProfile {
96 pub enabled: bool,
97 pub classify_calls: u64,
98 pub band_candidates: u64,
99 pub band_project_ms: f64,
100 pub band_trim_tests: u64,
101 pub band_trim_ms: f64,
102 pub rays: u64,
103 pub ray_faces: u64,
104 pub ray_csi_ms: f64,
105 pub ray_trim_tests: u64,
106 pub ray_trim_ms: f64,
107 pub coincident_calls: u64,
108 pub coincident_ms: f64,
109 pub classifier_builds: u64,
110 pub classifier_build_ms: f64,
111}
112
113thread_local! {
114 static CLASSIFY_PROFILE: std::cell::Cell<ClassifyProfile> =
115 const { std::cell::Cell::new(ClassifyProfile {
116 enabled: false, classify_calls: 0, band_candidates: 0, band_project_ms: 0.0,
117 band_trim_tests: 0, band_trim_ms: 0.0, rays: 0, ray_faces: 0, ray_csi_ms: 0.0,
118 ray_trim_tests: 0, ray_trim_ms: 0.0, coincident_calls: 0, coincident_ms: 0.0,
119 classifier_builds: 0, classifier_build_ms: 0.0,
120 }) };
121}
122
123fn classify_profile(update: impl FnOnce(&mut ClassifyProfile)) {
124 CLASSIFY_PROFILE.with(|cell| {
125 let mut profile = cell.get();
126 if profile.enabled {
127 update(&mut profile);
128 cell.set(profile);
129 }
130 });
131}
132
133fn classify_profile_started() -> Option<web_time::Instant> {
134 CLASSIFY_PROFILE
135 .with(|cell| cell.get().enabled)
136 .then(web_time::Instant::now)
137}
138
139fn classify_elapsed_ms(started: Option<web_time::Instant>) -> f64 {
140 started.map_or(0.0, |s| s.elapsed().as_secs_f64() * 1_000.0)
141}
142
143pub(crate) fn classify_profile_begin() {
145 let enabled = std::env::var("BREP_PROFILE").is_ok();
146 CLASSIFY_PROFILE.with(|cell| {
147 cell.set(ClassifyProfile {
148 enabled,
149 ..ClassifyProfile::default()
150 })
151 });
152}
153
154pub(crate) fn classify_profile_report(label: &str) {
157 let p = CLASSIFY_PROFILE.with(|cell| cell.get());
158 if !p.enabled {
159 return;
160 }
161 eprintln!(
162 "classify.profile {label} calls={} builds={} build_ms={:.2} band: candidates={} project_ms={:.2} trim_tests={} trim_ms={:.2} rays={} ray_faces={} csi_ms={:.2} ray_trim_tests={} ray_trim_ms={:.2} coincident: calls={} ms={:.2}",
163 p.classify_calls, p.classifier_builds, p.classifier_build_ms, p.band_candidates,
164 p.band_project_ms, p.band_trim_tests, p.band_trim_ms, p.rays, p.ray_faces,
165 p.ray_csi_ms, p.ray_trim_tests, p.ray_trim_ms, p.coincident_calls, p.coincident_ms,
166 );
167 CLASSIFY_PROFILE.with(|cell| cell.set(ClassifyProfile::default()));
168}
169
170pub struct SolidClassifier<'a> {
175 faces: Vec<&'a FaceRecord>,
176 face_boxes: Vec<Aabb>,
177 bounds: Aabb,
178 bvh: Bvh,
179 tolerance: f64,
180}
181
182fn clip_segment_to_aabb(
186 start: Vec3,
187 direction: Vec3,
188 length: f64,
189 bounds: &Aabb,
190) -> Option<[f64; 2]> {
191 let mut s0 = 0.0f64;
192 let mut s1 = length;
193 for axis in 0..3 {
194 let (origin, delta, minimum, maximum) = match axis {
195 0 => (start.x, direction.x, bounds.minimum.x, bounds.maximum.x),
196 1 => (start.y, direction.y, bounds.minimum.y, bounds.maximum.y),
197 _ => (start.z, direction.z, bounds.minimum.z, bounds.maximum.z),
198 };
199 if delta.abs() <= 1e-15 {
200 if origin < minimum || origin > maximum {
201 return None;
202 }
203 continue;
204 }
205 let mut near = (minimum - origin) / delta;
206 let mut far = (maximum - origin) / delta;
207 if near > far {
208 std::mem::swap(&mut near, &mut far);
209 }
210 s0 = s0.max(near);
211 s1 = s1.min(far);
212 if s0 > s1 {
213 return None;
214 }
215 }
216 Some([s0, s1])
217}
218
219impl<'a> SolidClassifier<'a> {
220 pub fn new(solid: &'a BrepSolid, tolerance: f64) -> Result<Self, String> {
221 let build_started = classify_profile_started();
222 let faces: Vec<&FaceRecord> = solid.shells.iter().flat_map(|shell| &shell.faces).collect();
223 let face_boxes = faces
224 .iter()
225 .map(|face| Aabb::from_surface_controls(&face.surface))
226 .collect::<Result<Vec<_>, _>>()?;
227 let mut bounds = Aabb::empty();
228 for face_box in &face_boxes {
229 bounds.include(*face_box);
230 }
231 let bvh = Bvh::build(&face_boxes);
232 classify_profile(|p| {
233 p.classifier_builds += 1;
234 p.classifier_build_ms += classify_elapsed_ms(build_started);
235 });
236 Ok(Self {
237 faces,
238 face_boxes,
239 bounds,
240 bvh,
241 tolerance,
242 })
243 }
244
245 pub fn near_surface(&self, point: Vec3) -> Result<bool, String> {
251 let on_tolerance = self.tolerance * 10.0;
252 if !self.bounds.expanded(on_tolerance).contains(point) {
253 return Ok(false);
254 }
255 let mut candidates = Vec::new();
256 self.bvh
257 .containing_point(point, on_tolerance, &mut candidates);
258 for &index in &candidates {
259 let projection = project_point_to_surface(&self.faces[index].surface, point)?;
260 if projection.distance <= on_tolerance {
261 return Ok(true);
262 }
263 }
264 Ok(false)
265 }
266
267 pub fn within_band(&self, point: Vec3, band: f64) -> Result<bool, String> {
276 if !self.bounds.expanded(band).contains(point) {
277 return Ok(false);
278 }
279 let mut candidates = Vec::new();
280 self.bvh.containing_point(point, band, &mut candidates);
281 for &index in &candidates {
282 let projection = project_point_to_surface(&self.faces[index].surface, point)?;
283 if projection.distance <= band {
284 return Ok(true);
285 }
286 }
287 Ok(false)
288 }
289
290 pub fn classify(&self, point: Vec3) -> Result<PointClassification, String> {
291 classify_profile(|p| p.classify_calls += 1);
292 let tolerance = self.tolerance;
293 if !self.bounds.expanded(tolerance).contains(point) {
294 return Ok(PointClassification {
295 class: PointClass::Out,
296 on_normal: None,
297 });
298 }
299 let on_tolerance = tolerance * 10.0;
300 let mut candidates = Vec::new();
301 self.bvh
302 .containing_point(point, on_tolerance, &mut candidates);
303 candidates.sort_unstable();
304 let mut interior_normals: Vec<Vec3> = Vec::new();
311 let mut boundary_normals: Vec<Vec3> = Vec::new();
312 for &index in &candidates {
313 let face = self.faces[index];
314 let project_started = classify_profile_started();
315 let projection = project_point_to_surface(&face.surface, point)?;
316 classify_profile(|p| {
317 p.band_candidates += 1;
318 p.band_project_ms += classify_elapsed_ms(project_started);
319 });
320 if projection.distance > on_tolerance {
321 continue;
322 }
323 let uv_tolerance = face_uv_tolerance(face, projection.u, projection.v, on_tolerance);
324 let trim_started = classify_profile_started();
325 let trim = parameter_point_in_face(
326 face,
327 Vec2 {
328 x: projection.u,
329 y: projection.v,
330 },
331 uv_tolerance,
332 )?;
333 classify_profile(|p| {
334 p.band_trim_tests += 1;
335 p.band_trim_ms += classify_elapsed_ms(trim_started);
336 });
337 match trim {
338 PolygonClass::Inside => {
339 interior_normals.push(face_normal(face, projection.u, projection.v)?)
340 }
341 PolygonClass::Boundary => {
342 boundary_normals.push(face_normal(face, projection.u, projection.v)?)
343 }
344 PolygonClass::Outside => {}
345 }
346 }
347 let pool = if interior_normals.is_empty() {
348 &boundary_normals
349 } else {
350 &interior_normals
351 };
352 if !pool.is_empty() {
353 let mut sum = Vec3::default();
354 for normal in pool {
355 sum = sum.add(*normal);
356 }
357 if sum.length() > 1e-3 {
361 return Ok(PointClassification {
362 class: PointClass::On,
363 on_normal: Some(sum.normalized()?),
364 });
365 }
366 }
367 let directions = [
368 Vec3::new(0.577215, 0.618034, 0.532088),
369 Vec3::new(-0.707107, 0.267949, 0.654321),
370 Vec3::new(0.316228, -0.741657, 0.585786),
371 Vec3::new(-0.414214, -0.552786, -0.723607),
372 Vec3::new(0.9482, 0.11893, -0.29456),
373 Vec3::new(-0.13947, 0.90271, -0.40718),
374 Vec3::new(0.62361, -0.33912, -0.70414),
375 Vec3::new(0.20912, 0.51293, 0.83261),
376 ];
377 let ray_length = self.bounds.diagonal() * 3.0 + 1.0;
378 let require_agreement = std::env::var("BREP_CLASSIFY_RAY_AGREE").as_deref() != Ok("0");
388 let mut verdicts: Vec<PointClass> = Vec::new();
389 'directions: for direction in directions {
390 classify_profile(|p| p.rays += 1);
391 let direction = direction.normalized()?;
392 let ray_end = point.add(direction.scale(ray_length));
393 candidates.clear();
394 self.bvh
395 .intersecting_segment(point, ray_end, on_tolerance, &mut candidates);
396 candidates.sort_unstable();
397 let mut crossings = 0;
398 for &index in &candidates {
399 let face = self.faces[index];
400 let margin = on_tolerance.max(self.face_boxes[index].diagonal() * 1e-3);
410 let Some([span_start, span_end]) = clip_segment_to_aabb(
411 point,
412 direction,
413 ray_length,
414 &self.face_boxes[index].expanded(margin),
415 ) else {
416 continue;
417 };
418 let span_start = (span_start - margin).max(0.0);
419 let span_end = (span_end + margin).min(ray_length);
420 if span_end - span_start <= 1e-12 {
421 continue;
422 }
423 let sub_ray = make_line(
424 point.add(direction.scale(span_start)),
425 point.add(direction.scale(span_end)),
426 )?;
427 let csi_started = classify_profile_started();
428 let intersections = intersect_curve_surface(&sub_ray, &face.surface, tolerance)?;
429 classify_profile(|p| {
430 p.ray_faces += 1;
431 p.ray_csi_ms += classify_elapsed_ms(csi_started);
432 });
433 for intersection in intersections {
434 if intersection.point.sub(point).length() <= tolerance * 10.0 {
435 let trim_started = classify_profile_started();
436 let trim = parameter_point_in_face(
437 face,
438 Vec2 {
439 x: intersection.u,
440 y: intersection.v,
441 },
442 face_uv_tolerance(face, intersection.u, intersection.v, on_tolerance),
443 )?;
444 classify_profile(|p| {
445 p.ray_trim_tests += 1;
446 p.ray_trim_ms += classify_elapsed_ms(trim_started);
447 });
448 if trim != PolygonClass::Outside {
449 return Ok(PointClassification {
450 class: PointClass::On,
451 on_normal: Some(face_normal(face, intersection.u, intersection.v)?),
452 });
453 }
454 continue;
455 }
456 if intersection.tangential {
457 continue 'directions;
458 }
459 let trim_started = classify_profile_started();
460 let trim_class = parameter_point_in_face(
461 face,
462 Vec2 {
463 x: intersection.u,
464 y: intersection.v,
465 },
466 face_uv_tolerance(face, intersection.u, intersection.v, on_tolerance),
467 )?;
468 classify_profile(|p| {
469 p.ray_trim_tests += 1;
470 p.ray_trim_ms += classify_elapsed_ms(trim_started);
471 });
472 if std::env::var("BREP_DEBUG_CLASSIFY").is_ok() {
473 eprintln!(
474 "classify ray dir=({:.3},{:.3},{:.3}) face={} hit=({:.4},{:.4},{:.4}) t3d={:.4} uv=({:.6},{:.6}) trim={:?}",
475 direction.x, direction.y, direction.z,
476 face.id,
477 intersection.point.x, intersection.point.y, intersection.point.z,
478 intersection.point.sub(point).length(),
479 intersection.u, intersection.v,
480 trim_class
481 );
482 }
483 match trim_class {
484 PolygonClass::Boundary => continue 'directions,
485 PolygonClass::Inside => crossings += 1,
486 PolygonClass::Outside => {}
487 }
488 }
489 }
490 let verdict = if crossings % 2 == 1 {
491 PointClass::In
492 } else {
493 PointClass::Out
494 };
495 if std::env::var("BREP_DEBUG_CLASSIFY").is_ok() {
496 eprintln!(
497 "classify verdict dir=({:.3},{:.3},{:.3}) crossings={crossings} -> {verdict:?}",
498 direction.x, direction.y, direction.z
499 );
500 }
501 if !require_agreement || verdicts.contains(&verdict) {
502 return Ok(PointClassification {
503 class: verdict,
504 on_normal: None,
505 });
506 }
507 verdicts.push(verdict);
508 }
509 if let Some(&verdict) = verdicts.last() {
513 return Ok(PointClassification {
514 class: verdict,
515 on_normal: None,
516 });
517 }
518 Err("classifyPointVsSolid: no clean ray direction found".into())
519 }
520
521 pub fn coincident_on_normal(&self, point: Vec3) -> Result<Option<Vec3>, String> {
536 let started = classify_profile_started();
537 let result = self.coincident_on_normal_inner(point);
538 classify_profile(|p| {
539 p.coincident_calls += 1;
540 p.coincident_ms += classify_elapsed_ms(started);
541 });
542 result
543 }
544
545 fn coincident_on_normal_inner(&self, point: Vec3) -> Result<Option<Vec3>, String> {
546 let band = (self.tolerance * 10.0).max(self.bounds.diagonal() * 1e-7);
547 if !self.bounds.expanded(band).contains(point) {
548 return Ok(None);
549 }
550 let mut candidates = Vec::new();
551 self.bvh.containing_point(point, band, &mut candidates);
552 let mut interior_normals: Vec<Vec3> = Vec::new();
553 for &index in &candidates {
554 let face = self.faces[index];
555 let projection = project_point_to_surface(&face.surface, point)?;
556 if projection.distance > band {
557 continue;
558 }
559 let uv_tolerance = face_uv_tolerance(face, projection.u, projection.v, band);
560 if let PolygonClass::Inside = parameter_point_in_face(
561 face,
562 Vec2 {
563 x: projection.u,
564 y: projection.v,
565 },
566 uv_tolerance,
567 )? {
568 interior_normals.push(face_normal(face, projection.u, projection.v)?);
569 }
570 }
571 if interior_normals.is_empty() {
572 return Ok(None);
573 }
574 let mut sum = Vec3::default();
575 for normal in &interior_normals {
576 sum = sum.add(*normal);
577 }
578 if sum.length() <= 1e-3 {
579 return Ok(None);
580 }
581 Ok(Some(sum.normalized()?))
582 }
583}
584
585pub fn classify_point(
586 point: Vec3,
587 solid: &BrepSolid,
588 tolerance: f64,
589) -> Result<PointClassification, String> {
590 classify_profile_begin();
591 let result = SolidClassifier::new(solid, tolerance)?.classify(point);
592 classify_profile_report("classify_point");
593 result
594}