Skip to main content

ogeom_offset/
middle.rs

1//! The middle path of a pipe-like solid: the curve its cross-sections'
2//! centroids trace from one end face to the other.
3//!
4//! The solid is meshed once, and the march cuts that mesh with planes.
5//! Each station is found by predictor and corrector: a plane square to the
6//! current tangent one step ahead gives a first centroid, the chord to it
7//! turns the tangent (on a circular spine the chord's direction is the mean
8//! of its end tangents), and the plane square to the turned tangent through
9//! the centroid gives the next, until the station stops moving. A plane
10//! square to a tube's own spine cuts it in a section centred on the spine,
11//! so the stations sit on the spine to within the mesh's chord.
12//!
13//! The spine is fitted through the stations and then measured: planes
14//! square to the fitted curve between the stations are cut again, and the
15//! largest distance from a cut's centroid to the curve is the deviation
16//! reported. A deviation over the tolerance halves the step and marches
17//! again.
18//!
19//! A sharp (mitred) corner stops the march: past the point where a section
20//! square to the leg reaches the mitre, it takes in the next leg too and
21//! its centroid jumps aside. The march then runs back from the end face as
22//! well, each leg is kept up to the last station whose section cannot
23//! reach the mitre, and the two legs, carried on straight along their last
24//! tangents, must meet: there is the corner. The corner is measured too,
25//! by the section on the plane halving it, whose centroid a mitred tube
26//! has on the corner.
27
28use ogeom_algo::{Built, History, make_edge_between, make_vertex, make_wire};
29use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
30use ogeom_geom::{Curve, Curve3d as _, LineCurve};
31use ogeom_math::{Point, Vector};
32use ogeom_mesh::{Deflection, triangulate, triangulate_face};
33use ogeom_topo::{Filter, Model, Shape, ShapeType, Triangulation, explore};
34use std::collections::HashMap;
35
36/// A middle path and how closely it follows the solid's sections.
37#[derive(Debug, Clone)]
38pub struct MiddlePath {
39    /// The wire of the path, from the start face's centroid to the end
40    /// face's, and its history: both end faces generate it.
41    pub built: Built,
42    /// The largest distance, measured, from the centroid of a section cut
43    /// square to the path to the path itself.
44    pub deviation: f64,
45}
46
47/// The steps the march may halve before it gives up on the tolerance.
48const MAX_REFINEMENTS: usize = 5;
49
50/// The stations one march may place before it is taken to be lost.
51const MAX_STATIONS: usize = 4000;
52
53/// The most a station may turn the march's tangent, as the cosine of the
54/// angle: a turn sharper than this is a section that has run into the
55/// next leg past a sharp corner. A smooth spine turns by the step over its
56/// radius of curvature, which a halved step brings under it.
57const SHARPEST_TURN: f64 = core::f64::consts::FRAC_1_SQRT_2;
58
59/// The centre line of a pipe-like `solid`, from its face `start` to its
60/// face `end`.
61///
62/// Each point of the path is the centroid of the solid's section by a
63/// plane square to the path there. The path starts at the centroid of
64/// `start` and ends at the centroid of `end`, and it is a straight edge
65/// where every station lies on one line and a fitted spline otherwise.
66/// A tube with one sharp (mitred) corner has a path of two edges, one per
67/// leg, meeting where the legs carried on straight meet; the sections
68/// near the corner, which reach into the other leg, are not measured, and
69/// the section on the plane halving the corner is. `tolerance` bounds the
70/// reported deviation; the solid is meshed at a quarter of it.
71///
72/// # Errors
73///
74/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if
75/// `solid` is not a solid, either face is not one of its faces, the two
76/// faces are the same, the tolerance is not a positive distance, or the
77/// mesh does not close. [`OgeomError::NotDone`](ogeom_core::OgeomError::NotDone)
78/// if a section square to the march finds no material (the solid is not
79/// pipe-like between the faces), the march never reaches `end`, it turns
80/// back on itself where its legs do not meet at one corner, or the
81/// deviation stays over the tolerance after every refinement.
82pub fn middle_path(
83    model: &mut Model,
84    solid: &Shape,
85    start: &Shape,
86    end: &Shape,
87    tolerance: f64,
88    tol: Tolerances,
89) -> OgeomResult<MiddlePath> {
90    if !tolerance.is_finite() || tolerance <= tol.confusion() {
91        ogeom_bail!(
92            Construction,
93            "a middle path to {tolerance} is not a distance"
94        );
95    }
96    if model.kind_of(solid)? != ShapeType::Solid {
97        ogeom_bail!(Construction, "a middle path runs through a solid");
98    }
99    let faces = explore(model, solid, Filter::OfType(ShapeType::Face))?;
100    for face in [start, end] {
101        if !faces.iter().any(|f| f.is_same(face)) {
102            ogeom_bail!(Construction, "the end faces must be faces of the solid");
103        }
104    }
105    if start.is_same(end) {
106        ogeom_bail!(Construction, "a middle path needs two different end faces");
107    }
108
109    let deflection = Deflection {
110        chord: tolerance * 0.25,
111        ..Deflection::default()
112    };
113    let mesh = Slicer::new(triangulate(model, solid, deflection, tol)?);
114    if !mesh.closed {
115        ogeom_bail!(
116            Construction,
117            "the solid's mesh does not close, so its sections are not regions"
118        );
119    }
120    let (c0, n0, area0) = face_centroid(model, start, deflection, tol)?;
121    let (ce, ne, _) = face_centroid(model, end, deflection, tol)?;
122    let size = (area0 / core::f64::consts::PI).sqrt();
123    if size <= tol.confusion() {
124        ogeom_bail!(Construction, "the start face has no area to start from");
125    }
126
127    // Into the solid: the side where a nearby plane cuts material round
128    // an end face's centroid.
129    let probe = size * 0.05;
130    let inward = |c: Point, n: Vector| {
131        if mesh.section(c + n * probe, n, tol).is_some() {
132            Some(n)
133        } else if mesh.section(c - n * probe, n, tol).is_some() {
134            Some(-n)
135        } else {
136            None
137        }
138    };
139    let Some(t0) = inward(c0, n0) else {
140        ogeom_bail!(NotDone, "no material lies behind the start face");
141    };
142
143    let mut step = size;
144    let mut last = Err("the middle path was not marched");
145    for _ in 0..=MAX_REFINEMENTS {
146        let forward = march(&mesh, c0, t0, ce, step, tol)?;
147        let path = if forward.stopped.is_none() {
148            let curve = fit_stations(&forward.stations, tolerance, tol)?;
149            let deviation = measure(&mesh, &curve, forward.stations.len(), tol)?;
150            Some((vec![curve], deviation))
151        } else if let Some(te) = inward(ce, ne) {
152            let backward = march(&mesh, ce, te, c0, step, tol)?;
153            if backward.stopped.is_none() {
154                let mut stations = backward.stations;
155                stations.reverse();
156                let curve = fit_stations(&stations, tolerance, tol)?;
157                let deviation = measure(&mesh, &curve, stations.len(), tol)?;
158                Some((vec![curve], deviation))
159            } else {
160                cornered(&mesh, &forward, &backward, tolerance, tol)?
161            }
162        } else {
163            None
164        };
165        match path {
166            Some((curves, deviation)) if deviation <= tolerance => {
167                return build(model, curves, start, end, deviation, tol);
168            }
169            Some((_, deviation)) => last = Ok(deviation),
170            None => {
171                if let Some(stopped) = forward.stopped {
172                    last = Err(stopped);
173                }
174            }
175        }
176        step *= 0.5;
177    }
178    match last {
179        Ok(deviation) => ogeom_bail!(
180            NotDone,
181            "the middle path reached a deviation of {deviation} against a tolerance of {tolerance}"
182        ),
183        Err(stopped) => ogeom_bail!(NotDone, "{stopped}"),
184    }
185}
186
187/// The stations of one march, each with the march's tangent there and the
188/// reach of its section (the farthest its outline stands from its
189/// centroid), and why the march stopped short of the end, if it did.
190struct Marched {
191    stations: Vec<Point>,
192    tangents: Vec<Vector>,
193    reaches: Vec<f64>,
194    stopped: Option<&'static str>,
195}
196
197/// March from `c0` along `t0` until the end centroid `ce` is within a step
198/// and ahead. Near is not enough: a ring bent almost shut starts beside its
199/// own end face. A section that finds no material, a station that does not
200/// advance, or one that turns the tangent sharper than a smooth spine
201/// would, stops the march there: past a sharp corner each of these is a
202/// section run into the next leg, and the stations before it are kept.
203fn march(
204    mesh: &Slicer,
205    c0: Point,
206    t0: Vector,
207    ce: Point,
208    step: f64,
209    tol: Tolerances,
210) -> OgeomResult<Marched> {
211    let first = mesh
212        .section(c0 + t0 * (step * 1e-3), t0, tol)
213        .map_or(0.0, |s| s.reach);
214    let mut marched = Marched {
215        stations: vec![c0],
216        tangents: vec![t0],
217        reaches: vec![first],
218        stopped: None,
219    };
220    let (mut c, mut t) = (c0, t0);
221    while c.distance(ce) > step * 1.25 || (ce - c).dot(t) < 0.5 * c.distance(ce) {
222        if marched.stations.len() > MAX_STATIONS {
223            ogeom_bail!(NotDone, "the middle path never reached the end face");
224        }
225        let Some(mut q) = mesh.section(c + t * step, t, tol) else {
226            marched.stopped = Some("a section square to the middle path finds no material");
227            return Ok(marched);
228        };
229        let mut turned = t;
230        for _ in 0..8 {
231            let chord = (q.centre - c).normalized(tol)?;
232            turned = (chord * (2.0 * t.dot(chord)) - t).normalized(tol)?;
233            let Some(next) = mesh.section(q.centre, turned, tol) else {
234                break;
235            };
236            let moved = next.centre.distance(q.centre);
237            q = next;
238            if moved <= tol.confusion() {
239                break;
240            }
241        }
242        if (q.centre - c).dot(t) <= 0.0 || turned.dot(t) < SHARPEST_TURN {
243            marched.stopped = Some("the middle path turned back on itself");
244            return Ok(marched);
245        }
246        marched.stations.push(q.centre);
247        marched.tangents.push(turned);
248        marched.reaches.push(q.reach);
249        (c, t) = (q.centre, turned);
250    }
251    marched.stations.push(ce);
252    marched.tangents.push(t);
253    marched.reaches.push(0.0);
254    Ok(marched)
255}
256
257/// The path of two legs meeting at a sharp corner: the stations marched
258/// from the start (`forward`) and from the end (`backward`), each cut back
259/// to those whose section cannot reach the mitre, and the corner where
260/// the two legs carried on straight along their last tangents meet. The
261/// two curves, start to corner and corner to end, and the deviation
262/// measured on both and at the corner; `None` where the legs do not meet
263/// within `tolerance` ahead of both.
264fn cornered(
265    mesh: &Slicer,
266    forward: &Marched,
267    backward: &Marched,
268    tolerance: f64,
269    tol: Tolerances,
270) -> OgeomResult<Option<(Vec<Curve>, f64)>> {
271    let (mut a, mut b) = (forward.stations.len(), backward.stations.len());
272    let mut corner = None;
273    for _ in 0..8 {
274        if a == 0 || b == 0 {
275            return Ok(None);
276        }
277        let (pa, da) = leg_end(forward, a, tolerance, tol)?;
278        let (pb, db) = leg_end(backward, b, tolerance, tol)?;
279        let Some(k) = meeting(pa, da, pb, db, tolerance) else {
280            return Ok(None);
281        };
282        // A plane square to a leg at a distance s before the corner reaches
283        // the plane halving it where the section reaches past s over the
284        // tangent of half the turn.
285        let cos_turn = da.dot(-db).clamp(-1.0, 1.0);
286        if cos_turn <= -0.99 {
287            return Ok(None);
288        }
289        let half_turn = ((1.0 - cos_turn) / (1.0 + cos_turn)).sqrt();
290        let clear = |marched: &Marched, upto: usize| {
291            (0..upto)
292                .take_while(|&i| {
293                    marched.stations[i].distance(k)
294                        > marched.reaches[i] * half_turn * 1.05 + tolerance
295                })
296                .count()
297        };
298        let (na, nb) = (clear(forward, a).max(1), clear(backward, b).max(1));
299        corner = Some((k, da, db));
300        if (na, nb) == (a, b) {
301            break;
302        }
303        (a, b) = (na, nb);
304    }
305    let Some((k, da, db)) = corner else {
306        return Ok(None);
307    };
308
309    let mut first: Vec<Point> = forward.stations[..a].to_vec();
310    first.push(k);
311    let mut second: Vec<Point> = backward.stations[..b].to_vec();
312    second.push(k);
313    second.reverse();
314    let legs = [
315        fit_stations(&first, tolerance, tol)?,
316        fit_stations(&second, tolerance, tol)?,
317    ];
318    // Each leg is measured where its sections stand clear of the corner;
319    // the corner by the section halving it.
320    let reach = forward.reaches[..a]
321        .iter()
322        .chain(&backward.reaches[..b])
323        .fold(0.0_f64, |m, r| m.max(*r));
324    let cos_turn = da.dot(-db).clamp(-1.0, 1.0);
325    let clear = reach * ((1.0 - cos_turn) / (1.0 + cos_turn)).sqrt() * 1.05 + tolerance;
326    let mut worst = 0.0_f64;
327    for (leg, stations) in legs.iter().zip([a, b]) {
328        worst = worst.max(measure_where(
329            mesh,
330            leg,
331            stations,
332            |p| p.distance(k) > clear,
333            tol,
334        )?);
335    }
336    let Some(halving) = mesh.section(k, da - db, tol) else {
337        return Ok(None);
338    };
339    worst = worst.max(halving.centre.distance(k));
340    Ok(Some((legs.to_vec(), worst)))
341}
342
343/// Where a march's first `upto` stations leave off, and the direction they
344/// leave in: the end of the curve fitted through them, which on a straight
345/// leg is the line from the first station to the last.
346fn leg_end(
347    marched: &Marched,
348    upto: usize,
349    tolerance: f64,
350    tol: Tolerances,
351) -> OgeomResult<(Point, Vector)> {
352    if upto < 2 {
353        return Ok((marched.stations[0], marched.tangents[0]));
354    }
355    let curve = fit_stations(&marched.stations[..upto], tolerance, tol)?;
356    let (_, hi) = curve.domain();
357    Ok((
358        curve.point_at(hi, tol)?,
359        curve.d1_at(hi, tol)?.normalized(tol)?,
360    ))
361}
362
363/// Where the lines through `pa` along `da` and through `pb` along `db`
364/// meet: the middle of their closest points, both ahead of their starts
365/// and within `tolerance` of each other.
366fn meeting(pa: Point, da: Vector, pb: Point, db: Vector, tolerance: f64) -> Option<Point> {
367    let w = pa - pb;
368    let (aa, ab, bb) = (da.dot(da), da.dot(db), db.dot(db));
369    let (aw, bw) = (da.dot(w), db.dot(w));
370    let det = aa * bb - ab * ab;
371    if det <= 1e-12 * aa * bb {
372        return None;
373    }
374    let s = (ab * bw - bb * aw) / det;
375    let u = (aa * bw - ab * aw) / det;
376    if s < 0.0 || u < 0.0 {
377        return None;
378    }
379    let (qa, qb) = (pa + da * s, pb + db * u);
380    (qa.distance(qb) <= tolerance).then(|| qa.lerp(qb, 0.5))
381}
382
383/// A straight line where the stations are collinear, else a fitted spline.
384fn fit_stations(stations: &[Point], tolerance: f64, tol: Tolerances) -> OgeomResult<Curve> {
385    let (first, last) = (stations[0], stations[stations.len() - 1]);
386    let axis = (last - first).normalized(tol)?;
387    let off_line = stations
388        .iter()
389        .map(|p| {
390            let d = *p - first;
391            (d - axis * d.dot(axis)).magnitude()
392        })
393        .fold(0.0_f64, f64::max);
394    if off_line <= tolerance * 0.05 {
395        return Ok(Curve::Line(LineCurve::segment(first, last, tol)?));
396    }
397    let fitted = ogeom_geom::fit::fit_points(stations, 3, tolerance * 0.1, tol)?;
398    Ok(Curve::BSpline(fitted.curve))
399}
400
401/// The largest distance from a section's centroid to the curve, cut square
402/// to the curve at points between the stations.
403fn measure(mesh: &Slicer, curve: &Curve, stations: usize, tol: Tolerances) -> OgeomResult<f64> {
404    measure_where(mesh, curve, stations, |_| true, tol)
405}
406
407/// As [`measure`], at the points of the curve `keep` holds.
408fn measure_where(
409    mesh: &Slicer,
410    curve: &Curve,
411    stations: usize,
412    keep: impl Fn(Point) -> bool,
413    tol: Tolerances,
414) -> OgeomResult<f64> {
415    let (lo, hi) = curve.domain();
416    let samples = (stations * 2).max(8);
417    let mut worst = 0.0_f64;
418    // The ends stand on the end faces, whose centroids are the path's ends
419    // by construction; only the inside is measured.
420    for i in 1..samples {
421        #[allow(clippy::cast_precision_loss)]
422        let u = lo + (hi - lo) * (i as f64) / (samples as f64);
423        let p = curve.point_at(u, tol)?;
424        if !keep(p) {
425            continue;
426        }
427        let d = curve.d1_at(u, tol)?.normalized(tol)?;
428        let Some(q) = mesh.section(p, d, tol) else {
429            ogeom_bail!(
430                NotDone,
431                "a section square to the fitted path finds no material"
432            );
433        };
434        worst = worst.max(q.centre.distance(p));
435    }
436    Ok(worst)
437}
438
439/// The path's wire: its curves in order, each an edge from the end of the
440/// one before.
441fn build(
442    model: &mut Model,
443    curves: Vec<Curve>,
444    start: &Shape,
445    end: &Shape,
446    deviation: f64,
447    tol: Tolerances,
448) -> OgeomResult<MiddlePath> {
449    let mut edges = Vec::with_capacity(curves.len());
450    let mut from: Option<Shape> = None;
451    for curve in curves {
452        let domain = curve.domain();
453        let from_vertex = match from.take() {
454            Some(v) => v,
455            None => make_vertex(model, curve.point_at(domain.0, tol)?).shape,
456        };
457        let to_vertex = make_vertex(model, curve.point_at(domain.1, tol)?).shape;
458        let edge = make_edge_between(model, curve, domain, &from_vertex, &to_vertex, tol)?.shape;
459        edges.push(edge);
460        from = Some(to_vertex);
461    }
462    let wire = make_wire(model, &edges, tol)?.shape;
463    let mut history = History::new();
464    history.generate(start, wire.clone());
465    history.generate(end, wire.clone());
466    Ok(MiddlePath {
467        built: Built::new(wire, history),
468        deviation,
469    })
470}
471
472/// A face's area centroid, its mean outward normal and its area, off its
473/// mesh.
474fn face_centroid(
475    model: &Model,
476    face: &Shape,
477    deflection: Deflection,
478    tol: Tolerances,
479) -> OgeomResult<(Point, Vector, f64)> {
480    let mesh = triangulate_face(model, face, deflection, tol)?;
481    let (mut weighted, mut normal, mut area) = (Vector::ZERO, Vector::ZERO, 0.0);
482    for t in &mesh.triangles {
483        let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
484        let n = (b - a).cross(c - a) * 0.5;
485        let da = n.magnitude();
486        weighted += (a.to_vector() + b.to_vector() + c.to_vector()) * (da / 3.0);
487        normal += n;
488        area += da;
489    }
490    if area <= 0.0 {
491        ogeom_bail!(Construction, "an end face has no area");
492    }
493    Ok((
494        Point::from_vector(weighted * (1.0 / area)),
495        normal.normalized(tol)?,
496        area,
497    ))
498}
499
500/// A closed triangle mesh, cut by planes.
501struct Slicer {
502    mesh: Triangulation,
503    closed: bool,
504}
505
506impl Slicer {
507    fn new(mesh: Triangulation) -> Self {
508        let closed = !mesh.triangles.is_empty() && mesh.is_closed();
509        Self { mesh, closed }
510    }
511
512    /// The region the plane through `origin` square to `normal` cuts from
513    /// the solid, taking the outer loop round `origin` with the loops
514    /// inside that as holes: its centroid, and the farthest the outer loop
515    /// stands from it. `None` where no loop holds `origin`: the point is
516    /// not inside the material.
517    fn section(&self, origin: Point, normal: Vector, tol: Tolerances) -> Option<Section> {
518        let n = normal.normalized(tol).ok()?;
519        let helper = if n.x.abs() < 0.9 {
520            Vector::X
521        } else {
522            Vector::Y
523        };
524        let e1 = n.cross(helper).normalized(tol).ok()?;
525        let e2 = n.cross(e1);
526        let flat = |p: Point| {
527            let d = p - origin;
528            (d.dot(e1), d.dot(e2))
529        };
530
531        let positions = &self.mesh.positions;
532        let side: Vec<f64> = positions.iter().map(|p| (*p - origin).dot(n)).collect();
533        // A vertex exactly on the plane counts as above it, so every
534        // crossed triangle has exactly two crossed sides.
535        let above = |i: u32| side[i as usize] >= 0.0;
536        let crossing = |a: u32, b: u32| {
537            let (da, db) = (side[a as usize], side[b as usize]);
538            let s = da / (da - db);
539            positions[a as usize].lerp(positions[b as usize], s)
540        };
541        let key = |a: u32, b: u32| if a < b { (a, b) } else { (b, a) };
542
543        // Each crossed triangle gives a segment from one crossed side to
544        // the other, oriented so that loops run consistently.
545        let mut next: HashMap<(u32, u32), (u32, u32)> = HashMap::new();
546        let mut points: HashMap<(u32, u32), Point> = HashMap::new();
547        for t in &self.mesh.triangles {
548            let ups = t.iter().filter(|&&i| above(i)).count();
549            if ups == 0 || ups == 3 {
550                continue;
551            }
552            let mut sides = [(0u32, 0u32); 2];
553            let mut found = 0;
554            for k in 0..3 {
555                let (a, b) = (t[k], t[(k + 1) % 3]);
556                if above(a) != above(b) && found < 2 {
557                    // The side leaving the upper half first, so the segment
558                    // runs the same way round every loop.
559                    sides[found] = (a, b);
560                    found += 1;
561                }
562            }
563            let (s0, s1) = if above(sides[0].0) {
564                (sides[0], sides[1])
565            } else {
566                (sides[1], sides[0])
567            };
568            points.insert(key(s0.0, s0.1), crossing(s0.0, s0.1));
569            points.insert(key(s1.0, s1.1), crossing(s1.0, s1.1));
570            next.insert(key(s0.0, s0.1), key(s1.0, s1.1));
571        }
572        if next.is_empty() {
573            return None;
574        }
575
576        // Chain into loops.
577        let mut loops: Vec<Vec<(f64, f64)>> = Vec::new();
578        let mut seen: HashMap<(u32, u32), ()> = HashMap::new();
579        let mut starts: Vec<(u32, u32)> = next.keys().copied().collect();
580        starts.sort_unstable();
581        for s in starts {
582            if seen.contains_key(&s) {
583                continue;
584            }
585            let mut ring = Vec::new();
586            let mut at = s;
587            loop {
588                seen.insert(at, ());
589                ring.push(flat(points[&at]));
590                match next.get(&at) {
591                    Some(&n) if n == s => break,
592                    Some(&n) if !seen.contains_key(&n) => at = n,
593                    _ => return None,
594                }
595            }
596            if ring.len() >= 3 {
597                loops.push(ring);
598            }
599        }
600
601        let measured: Vec<((f64, f64), f64)> = loops.iter().map(|l| area_centroid(l)).collect();
602        // The outer loop: the largest holding the origin. A plane through
603        // a point outside the solid may still cut it elsewhere, and that
604        // cut is not this station's section.
605        let outer = (0..loops.len())
606            .filter(|&i| contains(&loops[i], (0.0, 0.0)))
607            .max_by(|&a, &b| measured[a].1.abs().total_cmp(&measured[b].1.abs()))?;
608        let (mut sx, mut sy, mut sa) = {
609            let ((x, y), a) = measured[outer];
610            (x * a.abs(), y * a.abs(), a.abs())
611        };
612        for (i, ring) in loops.iter().enumerate() {
613            if i != outer && contains(&loops[outer], ring[0]) {
614                let ((x, y), a) = measured[i];
615                sx -= x * a.abs();
616                sy -= y * a.abs();
617                sa -= a.abs();
618            }
619        }
620        if sa <= 0.0 {
621            return None;
622        }
623        let (cx, cy) = (sx / sa, sy / sa);
624        let reach = loops[outer]
625            .iter()
626            .fold(0.0_f64, |m, &(x, y)| m.max((x - cx).hypot(y - cy)));
627        Some(Section {
628            centre: origin + e1 * cx + e2 * cy,
629            reach,
630        })
631    }
632}
633
634/// A plane section's centroid and the farthest its outline stands from it.
635struct Section {
636    centre: Point,
637    reach: f64,
638}
639
640/// A polygon's area centroid and signed area.
641fn area_centroid(ring: &[(f64, f64)]) -> ((f64, f64), f64) {
642    let (mut a, mut cx, mut cy) = (0.0, 0.0, 0.0);
643    for i in 0..ring.len() {
644        let (p, q) = (ring[i], ring[(i + 1) % ring.len()]);
645        let w = p.0 * q.1 - q.0 * p.1;
646        a += w;
647        cx += (p.0 + q.0) * w;
648        cy += (p.1 + q.1) * w;
649    }
650    a *= 0.5;
651    if a == 0.0 {
652        return (ring[0], 0.0);
653    }
654    ((cx / (6.0 * a), cy / (6.0 * a)), a)
655}
656
657/// Whether a point lies inside a polygon, by crossing parity.
658fn contains(ring: &[(f64, f64)], p: (f64, f64)) -> bool {
659    let mut inside = false;
660    for i in 0..ring.len() {
661        let (a, b) = (ring[i], ring[(i + 1) % ring.len()]);
662        if (a.1 > p.1) != (b.1 > p.1) {
663            let x = a.0 + (p.1 - a.1) / (b.1 - a.1) * (b.0 - a.0);
664            if x > p.0 {
665                inside = !inside;
666            }
667        }
668    }
669    inside
670}