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 let Ok(derivatives) = face.surface.derivatives(u, v, 1) else {
54 return spatial;
55 };
56 let band = crate::tolerance::surface_uv_tolerance(
57 spatial,
58 derivatives[1][0].length(),
59 derivatives[0][1].length(),
60 );
61 let cap = match (face.surface.domain_u(), face.surface.domain_v()) {
62 (Ok([u0, u1]), Ok([v0, v1])) => ((u1 - u0).min(v1 - v0) * 0.05).max(1e-12),
63 _ => f64::INFINITY,
64 };
65 band.min(cap)
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
69#[serde(rename_all = "lowercase")]
70pub enum PointClass {
71 In,
72 Out,
73 On,
74}
75
76#[derive(Clone, Copy, Debug, Serialize)]
77pub struct PointClassification {
78 pub class: PointClass,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub on_normal: Option<Vec3>,
81}
82
83pub struct SolidClassifier<'a> {
88 faces: Vec<&'a FaceRecord>,
89 face_boxes: Vec<Aabb>,
90 bounds: Aabb,
91 bvh: Bvh,
92 tolerance: f64,
93}
94
95fn clip_segment_to_aabb(
99 start: Vec3,
100 direction: Vec3,
101 length: f64,
102 bounds: &Aabb,
103) -> Option<[f64; 2]> {
104 let mut s0 = 0.0f64;
105 let mut s1 = length;
106 for axis in 0..3 {
107 let (origin, delta, minimum, maximum) = match axis {
108 0 => (start.x, direction.x, bounds.minimum.x, bounds.maximum.x),
109 1 => (start.y, direction.y, bounds.minimum.y, bounds.maximum.y),
110 _ => (start.z, direction.z, bounds.minimum.z, bounds.maximum.z),
111 };
112 if delta.abs() <= 1e-15 {
113 if origin < minimum || origin > maximum {
114 return None;
115 }
116 continue;
117 }
118 let mut near = (minimum - origin) / delta;
119 let mut far = (maximum - origin) / delta;
120 if near > far {
121 std::mem::swap(&mut near, &mut far);
122 }
123 s0 = s0.max(near);
124 s1 = s1.min(far);
125 if s0 > s1 {
126 return None;
127 }
128 }
129 Some([s0, s1])
130}
131
132impl<'a> SolidClassifier<'a> {
133 pub fn new(solid: &'a BrepSolid, tolerance: f64) -> Result<Self, String> {
134 let faces: Vec<&FaceRecord> = solid.shells.iter().flat_map(|shell| &shell.faces).collect();
135 let face_boxes = faces
136 .iter()
137 .map(|face| Aabb::from_surface_controls(&face.surface))
138 .collect::<Result<Vec<_>, _>>()?;
139 let mut bounds = Aabb::empty();
140 for face_box in &face_boxes {
141 bounds.include(*face_box);
142 }
143 let bvh = Bvh::build(&face_boxes);
144 Ok(Self {
145 faces,
146 face_boxes,
147 bounds,
148 bvh,
149 tolerance,
150 })
151 }
152
153 pub fn near_surface(&self, point: Vec3) -> Result<bool, String> {
159 let on_tolerance = self.tolerance * 10.0;
160 if !self.bounds.expanded(on_tolerance).contains(point) {
161 return Ok(false);
162 }
163 let mut candidates = Vec::new();
164 self.bvh
165 .containing_point(point, on_tolerance, &mut candidates);
166 for &index in &candidates {
167 let projection = project_point_to_surface(&self.faces[index].surface, point)?;
168 if projection.distance <= on_tolerance {
169 return Ok(true);
170 }
171 }
172 Ok(false)
173 }
174
175 pub fn within_band(&self, point: Vec3, band: f64) -> Result<bool, String> {
184 if !self.bounds.expanded(band).contains(point) {
185 return Ok(false);
186 }
187 let mut candidates = Vec::new();
188 self.bvh.containing_point(point, band, &mut candidates);
189 for &index in &candidates {
190 let projection = project_point_to_surface(&self.faces[index].surface, point)?;
191 if projection.distance <= band {
192 return Ok(true);
193 }
194 }
195 Ok(false)
196 }
197
198 pub fn classify(&self, point: Vec3) -> Result<PointClassification, String> {
199 let tolerance = self.tolerance;
200 if !self.bounds.expanded(tolerance).contains(point) {
201 return Ok(PointClassification {
202 class: PointClass::Out,
203 on_normal: None,
204 });
205 }
206 let on_tolerance = tolerance * 10.0;
207 let mut candidates = Vec::new();
208 self.bvh
209 .containing_point(point, on_tolerance, &mut candidates);
210 candidates.sort_unstable();
211 let mut interior_normals: Vec<Vec3> = Vec::new();
218 let mut boundary_normals: Vec<Vec3> = Vec::new();
219 for &index in &candidates {
220 let face = self.faces[index];
221 let projection = project_point_to_surface(&face.surface, point)?;
222 if projection.distance > on_tolerance {
223 continue;
224 }
225 let uv_tolerance = face_uv_tolerance(face, projection.u, projection.v, on_tolerance);
226 match parameter_point_in_face(
227 face,
228 Vec2 {
229 x: projection.u,
230 y: projection.v,
231 },
232 uv_tolerance,
233 )? {
234 PolygonClass::Inside => {
235 interior_normals.push(face_normal(face, projection.u, projection.v)?)
236 }
237 PolygonClass::Boundary => {
238 boundary_normals.push(face_normal(face, projection.u, projection.v)?)
239 }
240 PolygonClass::Outside => {}
241 }
242 }
243 let pool = if interior_normals.is_empty() {
244 &boundary_normals
245 } else {
246 &interior_normals
247 };
248 if !pool.is_empty() {
249 let mut sum = Vec3::default();
250 for normal in pool {
251 sum = sum.add(*normal);
252 }
253 if sum.length() > 1e-3 {
257 return Ok(PointClassification {
258 class: PointClass::On,
259 on_normal: Some(sum.normalized()?),
260 });
261 }
262 }
263 let directions = [
264 Vec3::new(0.577215, 0.618034, 0.532088),
265 Vec3::new(-0.707107, 0.267949, 0.654321),
266 Vec3::new(0.316228, -0.741657, 0.585786),
267 Vec3::new(-0.414214, -0.552786, -0.723607),
268 Vec3::new(0.9482, 0.11893, -0.29456),
269 Vec3::new(-0.13947, 0.90271, -0.40718),
270 Vec3::new(0.62361, -0.33912, -0.70414),
271 Vec3::new(0.20912, 0.51293, 0.83261),
272 ];
273 let ray_length = self.bounds.diagonal() * 3.0 + 1.0;
274 let require_agreement = std::env::var("BREP_CLASSIFY_RAY_AGREE").as_deref() != Ok("0");
284 let mut verdicts: Vec<PointClass> = Vec::new();
285 'directions: for direction in directions {
286 let direction = direction.normalized()?;
287 let ray_end = point.add(direction.scale(ray_length));
288 candidates.clear();
289 self.bvh
290 .intersecting_segment(point, ray_end, on_tolerance, &mut candidates);
291 candidates.sort_unstable();
292 let mut crossings = 0;
293 for &index in &candidates {
294 let face = self.faces[index];
295 let margin = on_tolerance.max(self.face_boxes[index].diagonal() * 1e-3);
305 let Some([span_start, span_end]) = clip_segment_to_aabb(
306 point,
307 direction,
308 ray_length,
309 &self.face_boxes[index].expanded(margin),
310 ) else {
311 continue;
312 };
313 let span_start = (span_start - margin).max(0.0);
314 let span_end = (span_end + margin).min(ray_length);
315 if span_end - span_start <= 1e-12 {
316 continue;
317 }
318 let sub_ray = make_line(
319 point.add(direction.scale(span_start)),
320 point.add(direction.scale(span_end)),
321 )?;
322 for intersection in intersect_curve_surface(&sub_ray, &face.surface, tolerance)? {
323 if intersection.point.sub(point).length() <= tolerance * 10.0 {
324 let trim = parameter_point_in_face(
325 face,
326 Vec2 {
327 x: intersection.u,
328 y: intersection.v,
329 },
330 face_uv_tolerance(face, intersection.u, intersection.v, on_tolerance),
331 )?;
332 if trim != PolygonClass::Outside {
333 return Ok(PointClassification {
334 class: PointClass::On,
335 on_normal: Some(face_normal(face, intersection.u, intersection.v)?),
336 });
337 }
338 continue;
339 }
340 if intersection.tangential {
341 continue 'directions;
342 }
343 let trim_class = parameter_point_in_face(
344 face,
345 Vec2 {
346 x: intersection.u,
347 y: intersection.v,
348 },
349 face_uv_tolerance(face, intersection.u, intersection.v, on_tolerance),
350 )?;
351 if std::env::var("BREP_DEBUG_CLASSIFY").is_ok() {
352 eprintln!(
353 "classify ray dir=({:.3},{:.3},{:.3}) face={} hit=({:.4},{:.4},{:.4}) t3d={:.4} uv=({:.6},{:.6}) trim={:?}",
354 direction.x, direction.y, direction.z,
355 face.id,
356 intersection.point.x, intersection.point.y, intersection.point.z,
357 intersection.point.sub(point).length(),
358 intersection.u, intersection.v,
359 trim_class
360 );
361 }
362 match trim_class {
363 PolygonClass::Boundary => continue 'directions,
364 PolygonClass::Inside => crossings += 1,
365 PolygonClass::Outside => {}
366 }
367 }
368 }
369 let verdict = if crossings % 2 == 1 {
370 PointClass::In
371 } else {
372 PointClass::Out
373 };
374 if std::env::var("BREP_DEBUG_CLASSIFY").is_ok() {
375 eprintln!(
376 "classify verdict dir=({:.3},{:.3},{:.3}) crossings={crossings} -> {verdict:?}",
377 direction.x, direction.y, direction.z
378 );
379 }
380 if !require_agreement || verdicts.contains(&verdict) {
381 return Ok(PointClassification {
382 class: verdict,
383 on_normal: None,
384 });
385 }
386 verdicts.push(verdict);
387 }
388 if let Some(&verdict) = verdicts.last() {
392 return Ok(PointClassification {
393 class: verdict,
394 on_normal: None,
395 });
396 }
397 Err("classifyPointVsSolid: no clean ray direction found".into())
398 }
399
400 pub fn coincident_on_normal(&self, point: Vec3) -> Result<Option<Vec3>, String> {
415 let band = (self.tolerance * 10.0).max(self.bounds.diagonal() * 1e-7);
416 if !self.bounds.expanded(band).contains(point) {
417 return Ok(None);
418 }
419 let mut candidates = Vec::new();
420 self.bvh.containing_point(point, band, &mut candidates);
421 let mut interior_normals: Vec<Vec3> = Vec::new();
422 for &index in &candidates {
423 let face = self.faces[index];
424 let projection = project_point_to_surface(&face.surface, point)?;
425 if projection.distance > band {
426 continue;
427 }
428 let uv_tolerance = face_uv_tolerance(face, projection.u, projection.v, band);
429 if let PolygonClass::Inside = parameter_point_in_face(
430 face,
431 Vec2 {
432 x: projection.u,
433 y: projection.v,
434 },
435 uv_tolerance,
436 )? {
437 interior_normals.push(face_normal(face, projection.u, projection.v)?);
438 }
439 }
440 if interior_normals.is_empty() {
441 return Ok(None);
442 }
443 let mut sum = Vec3::default();
444 for normal in &interior_normals {
445 sum = sum.add(*normal);
446 }
447 if sum.length() <= 1e-3 {
448 return Ok(None);
449 }
450 Ok(Some(sum.normalized()?))
451 }
452}
453
454pub fn classify_point(
455 point: Vec3,
456 solid: &BrepSolid,
457 tolerance: f64,
458) -> Result<PointClassification, String> {
459 SolidClassifier::new(solid, tolerance)?.classify(point)
460}