1use std::sync::OnceLock;
4
5use ogeom_core::{OgeomError, OgeomResult, Tolerances, parallel};
6use ogeom_geom::Surface as _;
7use ogeom_math::{Aabb, Point, Point2, Vector};
8use ogeom_mesh::{Deflection, face_boundary_in_chart_met};
9use ogeom_topo::{Model, Orientation, SameKey, Shape, ShapeType, explore_unique};
10
11use super::{Closest, DistanceOptions, ShapeFeature, signed, unit, unit_or_zero};
12use crate::classify::{Containment, SolidBoundary, segment_distance};
13use crate::measure::{curve_bounds, project_on_curve, project_on_surface};
14use crate::proximity::{Element, PointDistance, RING_ANGULAR, RING_CHORD, Trim, trim_side};
15
16pub fn to_shape(
45 model: &Model,
46 shape: &Shape,
47 points: &[Point],
48 normals: Option<&[Vector]>,
49 options: &DistanceOptions,
50 tol: Tolerances,
51) -> OgeomResult<Vec<Option<Closest<ShapeFeature>>>> {
52 let facing = options.checked(points.len(), normals)?;
53 let target = Target::of(model, shape, options.signed, tol)?;
54 let answers = parallel::map_ordered(points, |i, &p| {
55 let wanted = match (facing, normals) {
56 (Some(cos), Some(normals)) => match unit(normals[i]) {
57 Some(m) => Some((m, cos)),
58 None => return Ok(None),
59 },
60 _ => None,
61 };
62 let faces = wanted.map(|(m, cos)| move |n: Vector| n.dot(m) >= cos);
63 let faces = faces.as_ref().map(|f| f as &dyn Fn(Vector) -> bool);
64 target.closest(model, p, faces, options, tol)
65 });
66 answers.into_iter().collect()
67}
68
69type Rings = Vec<Vec<Point2>>;
71
72pub(crate) struct Target {
74 elements: PointDistance,
75 bounds: Vec<Aabb>,
77 faces_of: Vec<Vec<usize>>,
80 finer: Vec<Vec<OnceLock<Option<Rings>>>>,
83 chords: Vec<f64>,
85 solid: Option<SolidBoundary>,
88}
89
90pub(crate) struct Passed<'a> {
95 pub(crate) faces: &'a [Shape],
96 pub(crate) from: Point,
97}
98
99#[derive(Debug, Clone, Copy)]
102pub(crate) struct Found {
103 pub(crate) distance: f64,
104 pub(crate) point: Point,
105 element: usize,
106 normal: Vector,
107}
108
109impl Target {
110 pub(crate) fn of(
113 model: &Model,
114 shape: &Shape,
115 sign: bool,
116 tol: Tolerances,
117 ) -> OgeomResult<Self> {
118 let elements = PointDistance::of(model, shape, tol)?;
119 let list = elements.elements();
120 let mut places: ogeom_core::FastMap<SameKey, usize> = ogeom_core::FastMap::default();
121 let mut bounds = Vec::with_capacity(list.len());
122 let reach = tol.confusion().max(model.tolerances().confusion());
123 for (i, element) in list.iter().enumerate() {
124 let (held, bound) = match element {
125 Element::Vertex(v, p) => (v, Aabb::of_point(*p)),
126 Element::Edge(e, curve) => (e, curve_bounds(curve, tol)?),
127 Element::Face(f, _) => (f, crate::tight::face_bounds(model, f)?),
128 };
129 places.insert(SameKey(held.clone()), i);
130 bounds.push(bound.expanded(reach));
131 }
132 let mut faces_of = vec![Vec::new(); list.len()];
133 let mut finer = Vec::with_capacity(list.len());
134 let mut chords = Vec::new();
135 let mut chord = RING_CHORD;
136 while chord > tol.confusion() {
137 chord = (chord / 100.0).max(tol.confusion());
138 chords.push(chord);
139 }
140 for (i, element) in list.iter().enumerate() {
141 let Element::Face(face, _) = element else {
142 finer.push(Vec::new());
143 continue;
144 };
145 finer.push(chords.iter().map(|_| OnceLock::new()).collect());
146 for kind in [ShapeType::Edge, ShapeType::Vertex] {
147 for held in explore_unique(model, face, kind)? {
148 if let Some(&at) = places.get(&SameKey(held)) {
149 faces_of[at].push(i);
150 }
151 }
152 }
153 }
154 let solid = if sign {
155 let chord = crate::classify::exact_ring_chord(tol);
156 match SolidBoundary::of(model, shape, chord, tol) {
157 Ok(solid) => Some(solid),
158 Err(OgeomError::Construction(_)) => None,
160 Err(other) => return Err(other),
161 }
162 } else {
163 None
164 };
165 Ok(Self {
166 elements,
167 bounds,
168 faces_of,
169 finer,
170 chords,
171 solid,
172 })
173 }
174
175 fn normal(&self, face: usize, (u, v): (f64, f64), tol: Tolerances) -> Option<Vector> {
178 let Element::Face(shape, prepared) = &self.elements.elements()[face] else {
179 return None;
180 };
181 let n = Vector::from(prepared.world.normal_at(u, v, tol).ok()?);
182 Some(if shape.orientation() == Orientation::Reversed {
183 -n
184 } else {
185 n
186 })
187 }
188
189 fn normals_at(&self, element: usize, q: Point, tol: Tolerances) -> OgeomResult<Vec<Vector>> {
192 let mut out = Vec::with_capacity(self.faces_of[element].len());
193 for &face in &self.faces_of[element] {
194 let Element::Face(_, prepared) = &self.elements.elements()[face] else {
195 continue;
196 };
197 let foot = project_on_surface(&prepared.world, q, 32, tol)?;
198 out.extend(self.normal(face, foot.parameters, tol));
199 }
200 Ok(out)
201 }
202
203 fn closest(
204 &self,
205 model: &Model,
206 p: Point,
207 faces: Option<&dyn Fn(Vector) -> bool>,
208 options: &DistanceOptions,
209 tol: Tolerances,
210 ) -> OgeomResult<Option<Closest<ShapeFeature>>> {
211 if !(p.x.is_finite() && p.y.is_finite() && p.z.is_finite()) {
212 return Ok(None);
213 }
214 let Some(found) = self.nearest(model, p, faces, None, options.max, tol)? else {
215 return Ok(None);
216 };
217 let element = &self.elements.elements()[found.element];
218 let mut normal = found.normal;
219 let feature = match element {
220 Element::Vertex(v, _) => ShapeFeature::Vertex(v.clone()),
221 Element::Edge(e, _) => ShapeFeature::Edge(e.clone()),
222 Element::Face(f, _) => ShapeFeature::Face(f.clone()),
223 };
224 let mut distance = signed(found.distance, p, found.point, normal, options.signed);
225 if options.signed
226 && matches!(feature, ShapeFeature::Vertex(_))
227 && let Some(solid) = &self.solid
228 {
229 match solid.holds(model, p, tol)? {
230 Containment::In => distance = -found.distance,
231 Containment::Out => distance = found.distance,
232 Containment::On => {}
233 }
234 }
235 normal = unit_or_zero(normal);
236 Ok(Some(Closest {
237 distance,
238 point: found.point,
239 feature,
240 normal,
241 }))
242 }
243
244 pub(crate) fn nearest(
251 &self,
252 model: &Model,
253 p: Point,
254 faces: Option<&dyn Fn(Vector) -> bool>,
255 passed: Option<&Passed<'_>>,
256 max: f64,
257 tol: Tolerances,
258 ) -> OgeomResult<Option<Found>> {
259 let list = self.elements.elements();
260 let mut order: Vec<(f64, usize)> = self
261 .bounds
262 .iter()
263 .enumerate()
264 .map(|(i, b)| (b.distance_to(p), i))
265 .filter(|&(gap, _)| gap <= max)
266 .collect();
267 order.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)));
268 let mut best: Option<Found> = None;
269 let better = |best: &Option<Found>, distance: f64, element: usize| match best {
270 None => distance <= max,
271 Some(held) => {
272 distance < held.distance || (distance == held.distance && element < held.element)
273 }
274 };
275 let kept = |normals: &[Vector]| faces.is_none_or(|faces| normals.iter().any(|&n| faces(n)));
276 let skipped = |element: usize, q: Point| {
277 passed.is_some_and(|passed| {
278 segment_distance(q, passed.from, p) <= tol.confusion()
279 && self.held_by(element, passed.faces)
280 })
281 };
282 let sum = |normals: Vec<Vector>| normals.into_iter().fold(Vector::ZERO, |a, n| a + n);
283 let mut banded: Vec<Found> = Vec::new();
284 for (gap, i) in order {
285 if best.is_some_and(|held| gap > held.distance) {
286 break;
287 }
288 match &list[i] {
289 Element::Vertex(_, q) => {
290 let distance = p.distance(*q);
291 if !better(&best, distance, i) || skipped(i, *q) {
292 continue;
293 }
294 let normals = self.normals_at(i, *q, tol)?;
295 if kept(&normals) {
296 best = Some(Found {
297 distance,
298 point: *q,
299 element: i,
300 normal: sum(normals),
301 });
302 }
303 }
304 Element::Edge(_, curve) => {
305 let foot = project_on_curve(curve, p, 64, tol)?;
306 if !better(&best, foot.distance, i) || skipped(i, foot.point) {
307 continue;
308 }
309 let normals = self.normals_at(i, foot.point, tol)?;
310 if kept(&normals) {
311 best = Some(Found {
312 distance: foot.distance,
313 point: foot.point,
314 element: i,
315 normal: sum(normals),
316 });
317 }
318 }
319 Element::Face(_, prepared) => {
320 let foot = project_on_surface(&prepared.world, p, 32, tol)?;
321 if !better(&best, foot.distance, i) || skipped(i, foot.point) {
322 continue;
323 }
324 let Some(normal) = self.normal(i, foot.parameters, tol) else {
327 continue;
328 };
329 if !faces.is_none_or(|faces| faces(normal)) {
330 continue;
331 }
332 let found = Found {
333 distance: foot.distance,
334 point: foot.point,
335 element: i,
336 normal,
337 };
338 match trim_side(prepared, None, RING_CHORD, foot.point, tol)? {
339 Trim::Inside => best = Some(found),
340 Trim::Outside => {}
341 Trim::Band => banded.push(found),
342 }
343 }
344 }
345 }
346 banded.sort_by(|a, b| {
347 a.distance
348 .total_cmp(&b.distance)
349 .then(a.element.cmp(&b.element))
350 });
351 for found in banded {
352 if !better(&best, found.distance, found.element) {
353 break;
354 }
355 if self.inside_finer(model, found.element, found.point, tol)? {
356 best = Some(found);
357 break;
358 }
359 }
360 Ok(best)
361 }
362
363 fn held_by(&self, element: usize, faces: &[Shape]) -> bool {
366 let list = self.elements.elements();
367 let among = |face: usize| match &list[face] {
368 Element::Face(shape, _) => faces.iter().any(|own| own.is_same(shape)),
369 _ => false,
370 };
371 match &list[element] {
372 Element::Face(..) => among(element),
373 _ => {
374 let holding = &self.faces_of[element];
375 !holding.is_empty() && holding.iter().all(|&face| among(face))
376 }
377 }
378 }
379
380 fn inside_finer(
385 &self,
386 model: &Model,
387 face: usize,
388 q: Point,
389 tol: Tolerances,
390 ) -> OgeomResult<bool> {
391 let Element::Face(shape, prepared) = &self.elements.elements()[face] else {
392 return Ok(false);
393 };
394 for (level, &chord) in self.chords.iter().enumerate() {
395 let rings = self.finer[face][level].get_or_init(|| {
396 let coarse = Deflection::default().max_segments;
400 #[allow(
401 clippy::cast_possible_truncation,
402 clippy::cast_sign_loss,
403 clippy::cast_precision_loss
404 )]
405 let cap = (coarse as f64 * (RING_CHORD / chord).sqrt()).ceil() as usize;
406 let deflection = Deflection {
407 chord,
408 angular: RING_ANGULAR,
409 max_segments: cap,
410 ..Deflection::default()
411 };
412 face_boundary_in_chart_met(model, shape, deflection, tol)
415 .ok()
416 .flatten()
417 });
418 let Some(rings) = rings else {
419 return Ok(false);
420 };
421 match trim_side(prepared, Some(rings), chord, q, tol)? {
422 Trim::Inside => return Ok(true),
423 Trim::Outside => return Ok(false),
424 Trim::Band => {}
425 }
426 }
427 Ok(false)
428 }
429}