1mod exact;
35mod hull;
36mod lp;
37mod slp;
38
39use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
40use ogeom_math::{Axis, Circle2, Cylinder, Direction, Frame, Plane, Point, Point2, Sphere};
41
42use super::FitOptions;
43use exact::Ring;
44use slp::{Centre, Goal, Line};
45
46#[derive(Debug, Clone, PartialEq)]
48pub struct Zone<S> {
49 pub shape: S,
54 pub width: f64,
59 pub contacts: Vec<usize>,
62 pub optimal: bool,
65}
66
67pub fn flatness(points: &[Point], tol: Tolerances) -> OgeomResult<Zone<Plane>> {
75 let fit = super::plane(points, &FitOptions::default(), tol)?;
76 let origin = fit.shape.origin();
77 let normal = fit.shape.normal().vector();
78 let mut seeds = extremes(&fit.residuals);
79 seeds.extend(spanning(points));
80 let Some(slab) = exact::narrowest_slab(points, origin, &seeds) else {
81 ogeom_bail!(Construction, "the points lie on one line and span no plane");
82 };
83 let (n, low, high) = if slab.normal.dot(normal) < 0.0 {
84 (-slab.normal, -slab.high, -slab.low)
85 } else {
86 (slab.normal, slab.low, slab.high)
87 };
88 let slack = tol.confusion();
89 let contacts = touching(
90 points.iter().map(|p| n.dot(*p - origin)),
91 Some(low),
92 Some(high),
93 slack,
94 );
95 let middle = origin + n * ((low + high) / 2.0);
96 Ok(Zone {
97 shape: Plane::new(Frame::about(middle, Direction::new(n, tol)?)),
98 width: high - low,
99 contacts,
100 optimal: true,
101 })
102}
103
104pub fn straightness(points: &[Point], tol: Tolerances) -> OgeomResult<Zone<Axis>> {
111 let fit = super::line3(points, &FitOptions::default(), tol)?;
112 let start = line(fit.shape.location, fit.shape.direction, points)?;
113 let settled = slp::settle(start, points, Goal::Outer, reach(points), tol.confusion());
114 let (_, most) = span(&settled.distances);
115 let axis = Axis::new(settled.shape.origin, Direction::new(settled.shape.z, tol)?);
116 Ok(Zone {
117 shape: axis,
118 width: 2.0 * most,
119 contacts: touching(
120 settled.distances.iter().copied(),
121 None,
122 Some(most),
123 tol.confusion(),
124 ),
125 optimal: settled.optimal,
126 })
127}
128
129pub fn circularity(points: &[Point2], tol: Tolerances) -> OgeomResult<Zone<Circle2>> {
140 let fit = super::circle2(points, &FitOptions::default(), tol)?;
141 let mut seeds = extremes(&fit.residuals);
142 let lifted: Vec<Point> = points.iter().map(|p| p.to_3d()).collect();
143 seeds.extend(spanning(&lifted));
144 match exact::narrowest_ring(points, fit.shape.centre(), &seeds) {
145 Ring::Circles {
146 centre,
147 radius,
148 deviations,
149 inner,
150 outer,
151 } => Ok(Zone {
152 shape: Circle2::centred(centre, radius + (inner + outer) / 2.0, tol)?,
153 width: outer - inner,
154 contacts: touching(
155 deviations.into_iter(),
156 Some(inner),
157 Some(outer),
158 tol.confusion(),
159 ),
160 optimal: true,
161 }),
162 Ring::Straight { width } => ogeom_bail!(
163 Construction,
164 "the points lie between two lines {width} apart, narrower than any ring: \
165 they have no narrowest circle"
166 ),
167 }
168}
169
170pub fn cylindricity(points: &[Point], tol: Tolerances) -> OgeomResult<Zone<Cylinder>> {
177 revolved(points, Goal::Zone, tol)
178}
179
180pub fn inscribed_cylinder(points: &[Point], tol: Tolerances) -> OgeomResult<Zone<Cylinder>> {
187 revolved(points, Goal::Inner, tol)
188}
189
190pub fn circumscribed_cylinder(points: &[Point], tol: Tolerances) -> OgeomResult<Zone<Cylinder>> {
197 revolved(points, Goal::Outer, tol)
198}
199
200pub fn sphericity(points: &[Point], tol: Tolerances) -> OgeomResult<Zone<Sphere>> {
207 let fit = super::sphere(points, &FitOptions::default(), tol)?;
208 let slack = tol.confusion();
209 let settled = slp::settle(
210 Centre(fit.shape.centre()),
211 points,
212 Goal::Zone,
213 reach(points),
214 slack,
215 );
216 let (least, most) = span(&settled.distances);
217 Ok(Zone {
218 shape: Sphere::centred(settled.shape.0, (least + most) / 2.0, tol)?,
219 width: most - least,
220 contacts: touching(
221 settled.distances.iter().copied(),
222 Some(least),
223 Some(most),
224 slack,
225 ),
226 optimal: settled.optimal,
227 })
228}
229
230pub fn inscribed_circle(points: &[Point2], tol: Tolerances) -> OgeomResult<Zone<Circle2>> {
240 finite(points)?;
241 if points.len() < 3 {
242 ogeom_bail!(
243 Construction,
244 "an inscribed circle needs at least 3 points, got {}",
245 points.len()
246 );
247 }
248 let Some(centre) = exact::largest_empty_circle(points) else {
249 ogeom_bail!(
250 Construction,
251 "the points lie on one line and enclose no circle"
252 );
253 };
254 let distances: Vec<f64> = points.iter().map(|p| p.distance(centre)).collect();
255 let (least, most) = span(&distances);
256 Ok(Zone {
257 shape: Circle2::centred(centre, least, tol)?,
258 width: most - least,
259 contacts: touching(
260 distances.iter().copied(),
261 Some(least),
262 None,
263 tol.confusion(),
264 ),
265 optimal: true,
266 })
267}
268
269pub fn circumscribed_circle(points: &[Point2], tol: Tolerances) -> OgeomResult<Zone<Circle2>> {
278 finite(points)?;
279 if points.is_empty() {
280 ogeom_bail!(Construction, "a circumscribed circle needs a point");
281 }
282 let (centre, radius) = exact::enclosing_circle(points);
283 let distances: Vec<f64> = points.iter().map(|p| p.distance(centre)).collect();
284 let (least, most) = span(&distances);
285 Ok(Zone {
286 shape: Circle2::centred(centre, radius, tol)?,
287 width: most - least,
288 contacts: touching(distances.iter().copied(), None, Some(most), tol.confusion()),
289 optimal: true,
290 })
291}
292
293fn revolved(points: &[Point], goal: Goal, tol: Tolerances) -> OgeomResult<Zone<Cylinder>> {
295 let fit = super::cylinder(points, &FitOptions::default(), tol)?;
296 let frame = fit.shape.frame();
297 let start = line(frame.origin(), frame.z(), points)?;
298 let slack = tol.confusion();
299 let settled = slp::settle(start, points, goal, reach(points), slack);
300 let (least, most) = span(&settled.distances);
301 let (radius, low, high) = match goal {
302 Goal::Zone => ((least + most) / 2.0, Some(least), Some(most)),
303 Goal::Outer => (most, None, Some(most)),
304 Goal::Inner => (least, Some(least), None),
305 };
306 let axis = Frame::about(settled.shape.origin, Direction::new(settled.shape.z, tol)?);
307 Ok(Zone {
308 shape: Cylinder::new(axis, radius, tol)?,
309 width: most - least,
310 contacts: touching(settled.distances.iter().copied(), low, high, slack),
311 optimal: settled.optimal,
312 })
313}
314
315fn line(location: Point, direction: Direction, points: &[Point]) -> OgeomResult<Line> {
318 let z = direction.vector();
319 let centroid = Point::centroid(points)?;
320 let origin = location + z * (centroid - location).dot(z);
321 match Line::new(origin, z, points) {
322 Some(l) => Ok(l),
323 None => ogeom_bail!(Construction, "the fitted axis has no direction"),
324 }
325}
326
327fn reach(points: &[Point]) -> f64 {
330 let (mut lo, mut hi) = (points[0], points[0]);
331 for p in points {
332 lo = lo.min(*p);
333 hi = hi.max(*p);
334 }
335 lo.distance(hi)
336}
337
338fn span(distances: &[f64]) -> (f64, f64) {
339 distances
340 .iter()
341 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), d| {
342 (lo.min(*d), hi.max(*d))
343 })
344}
345
346fn touching(
349 values: impl Iterator<Item = f64>,
350 low: Option<f64>,
351 high: Option<f64>,
352 slack: f64,
353) -> Vec<usize> {
354 values
355 .enumerate()
356 .filter(|&(_, v)| {
357 low.is_some_and(|l| v <= l + slack) || high.is_some_and(|h| v >= h - slack)
358 })
359 .map(|(i, _)| i)
360 .collect()
361}
362
363fn extremes(residuals: &[f64]) -> Vec<usize> {
366 let mut out = slp::largest(residuals.len(), 4, |i| Some(residuals[i]));
367 out.extend(slp::largest(residuals.len(), 4, |i| Some(-residuals[i])));
368 out
369}
370
371fn spanning(points: &[Point]) -> Vec<usize> {
374 let farthest = |key: &dyn Fn(Point) -> f64| {
375 slp::largest(points.len(), 1, |i| Some(key(points[i])))
376 .first()
377 .copied()
378 .unwrap_or(0)
379 };
380 let a = farthest(&|p| p.distance(points[0]));
381 let b = farthest(&|p| p.distance(points[a]));
382 let along = points[b] - points[a];
383 let c = farthest(&|p| (p - points[a]).cross(along).magnitude());
384 vec![a, b, c]
385}
386
387fn finite(points: &[Point2]) -> OgeomResult<()> {
388 if let Some(i) = points
389 .iter()
390 .position(|p| !(p.x.is_finite() && p.y.is_finite()))
391 {
392 ogeom_bail!(Domain, "point {i} is not finite");
393 }
394 Ok(())
395}