Skip to main content

ogeom_heal/
same_parameter.rs

1//! Verifying (and where needed, widening into truth) the `same_parameter`
2//! claim.
3//!
4//! An edge carries several representations of one curve, and nearly every
5//! algorithm evaluates whichever is convenient, assuming the answers are
6//! interchangeable within the edge's tolerance. The flag that records this is
7//! set false whenever a representation is added: honest, but pessimistic:
8//! every primitive's edges claim a disagreement they do not have. This is the
9//! repair the flag's own documentation demands: measure the actual
10//! disagreement, and either confirm the claim or widen the edge's tolerance
11//! until the claim is true. Either way, afterwards the flag *means* something.
12
13use ogeom_core::{FastSet, OgeomResult, Tolerance, Tolerances};
14use ogeom_topo::{EdgeRepr, Filter, Model, NodeData, Shape, ShapeType, explore};
15
16/// What one repair pass did.
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub struct SameParameterReport {
19    /// Edges examined.
20    pub checked: usize,
21    /// Edges whose representations already agreed within tolerance.
22    pub agreed: usize,
23    /// Edges whose tolerance had to widen to make the claim true.
24    pub widened: usize,
25    /// Edges with no pcurves to disagree with; trivially true.
26    pub trivial: usize,
27}
28
29/// Verify every edge under `shape` and make its `same_parameter` flag true.
30///
31/// Each pcurve, on its surface where the representation places it, is
32/// lifted against the edge's own curve at matched parameters (the same
33/// linear range mapping the triangulator uses), and the worst gap decides:
34/// within the edge's tolerance, the claim is confirmed; beyond it, the
35/// tolerance widens to cover what was measured, which makes the claim true
36/// by making the tolerance honest. Degenerate edges and edges with no
37/// pcurves are trivially true. An edge whose pcurves cannot be measured
38/// keeps its flag as it was. The edges are measured in parallel and
39/// widened in order.
40///
41/// Nodes below `shape` that other shapes hold as well are copied first
42/// ([`Model::unshare`]), so the repair reaches no other shape.
43///
44/// # Errors
45///
46/// [`OgeomError::Dangling`](ogeom_core::OgeomError::Dangling) if the shape
47/// resolves to nothing.
48pub fn repair_same_parameter(
49    model: &mut Model,
50    shape: &Shape,
51    tol: Tolerances,
52) -> OgeomResult<SameParameterReport> {
53    model.unshare(shape)?;
54    let mut seen = FastSet::default();
55    let edges: Vec<Shape> = explore(model, shape, Filter::OfType(ShapeType::Edge))?
56        .into_iter()
57        .filter(|edge| seen.insert(edge.node()))
58        .collect();
59    let found = {
60        let model = &*model;
61        ogeom_core::parallel::map_ordered(&edges, |_, edge| measure(model, edge, tol))
62    };
63    let mut report = SameParameterReport::default();
64    for (edge, found) in edges.iter().zip(found) {
65        report.checked += 1;
66        match found {
67            Found::Absent | Found::Unmeasured => {}
68            Found::Trivial { flag } => {
69                report.trivial += 1;
70                if flag {
71                    set_flag(model, edge, true);
72                }
73            }
74            Found::Gap(worst) => {
75                let within = model
76                    .node(edge)
77                    .and_then(|n| n.data().as_edge())
78                    .map_or(0.0, |data| data.tolerance.get());
79                if worst <= within {
80                    report.agreed += 1;
81                } else {
82                    report.widened += 1;
83                    model.widen(edge, Tolerance::new(worst + tol.confusion())?)?;
84                }
85                set_flag(model, edge, true);
86            }
87        }
88    }
89    Ok(report)
90}
91
92/// What measuring one edge found.
93enum Found {
94    /// Not an edge in this model.
95    Absent,
96    /// Nothing to disagree: no curve in space, or no pcurve. `flag` where
97    /// the claim is true for it.
98    Trivial { flag: bool },
99    /// Pcurves that could not be measured against the curve.
100    Unmeasured,
101    /// The widest gap between a lifted pcurve and the curve.
102    Gap(f64),
103}
104
105fn measure(model: &Model, edge: &Shape, tol: Tolerances) -> Found {
106    let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
107        return Found::Absent;
108    };
109    let Some(EdgeRepr::Curve3d { curve, .. }) = data.curve3d() else {
110        // A degenerate edge's pcurve is its whole story. There is nothing
111        // for it to disagree with.
112        return Found::Trivial { flag: true };
113    };
114    if model.geometry().curve(*curve).is_none() {
115        return Found::Trivial { flag: false };
116    }
117    let any_pcurve = data.representations.iter().any(|repr| match repr {
118        EdgeRepr::PCurve { curve, surface, .. } => {
119            model.geometry().pcurve(*curve).is_some()
120                && model.geometry().surface(*surface).is_some()
121        }
122        EdgeRepr::Seam {
123            forward,
124            reversed,
125            surface,
126            ..
127        } => {
128            model.geometry().surface(*surface).is_some()
129                && [forward, reversed]
130                    .iter()
131                    .any(|pc| model.geometry().pcurve(**pc).is_some())
132        }
133        _ => false,
134    });
135    if !any_pcurve {
136        return Found::Trivial { flag: true };
137    }
138    match crate::upgrade::measure_edge(model, edge, true, tol) {
139        Some(worst) if worst.is_finite() => Found::Gap(worst),
140        _ => Found::Unmeasured,
141    }
142}
143
144/// Set an edge's `same_parameter` claim.
145fn set_flag(model: &mut Model, edge: &Shape, agrees: bool) {
146    if let Some(node) = model.node_mut(edge)
147        && let NodeData::Edge(data) = node.data_mut()
148    {
149        data.assert_same_parameter(agrees);
150    }
151}
152
153#[cfg(test)]
154#[allow(clippy::unwrap_used, clippy::expect_used)]
155mod tests {
156    use super::*;
157    use ogeom_math::Frame;
158
159    const T: Tolerances = Tolerances::millimetres();
160
161    fn all_flags_true(model: &Model, shape: &Shape) -> bool {
162        explore(model, shape, Filter::OfType(ShapeType::Edge))
163            .unwrap()
164            .iter()
165            .all(|e| {
166                model
167                    .node(e)
168                    .and_then(|n| n.data().as_edge())
169                    .is_some_and(ogeom_topo::EdgeData::same_parameter)
170            })
171    }
172
173    #[test]
174    fn a_primitives_edges_agree_and_the_flag_finally_says_so() {
175        let mut model = Model::new();
176        let solid = ogeom_algo::make_cylinder(&mut model, Frame::WORLD, 2.0, 5.0, T).unwrap();
177        assert!(
178            !all_flags_true(&model, &solid.shape),
179            "the builder is honest: unverified means false"
180        );
181        let report = repair_same_parameter(&mut model, &solid.shape, T).unwrap();
182        assert_eq!(report.widened, 0, "a native primitive has nothing to widen");
183        assert!(report.agreed > 0);
184        assert!(all_flags_true(&model, &solid.shape));
185    }
186
187    #[test]
188    fn a_disagreeing_pcurve_widens_the_tolerance_into_truth() {
189        use ogeom_geom::{Line2d, LineCurve, PlaneSurface};
190        use ogeom_math::{Plane, Point, Point2};
191        let mut model = Model::new();
192        let curve: ogeom_geom::Curve =
193            LineCurve::segment(Point::new(0.0, 0.0, 0.0), Point::new(10.0, 0.0, 0.0), T)
194                .unwrap()
195                .into();
196        let edge = ogeom_algo::make_edge(&mut model, curve, (0.0, 10.0), T)
197            .unwrap()
198            .shape;
199        let surface = model.geometry_mut().add_surface(
200            PlaneSurface::over(Plane::new(Frame::WORLD), (-20.0, 20.0), (-20.0, 20.0))
201                .unwrap()
202                .into(),
203        );
204        // A pcurve half a unit off the curve it claims to follow.
205        let off = Line2d::segment(Point2::new(0.0, 0.5), Point2::new(10.0, 0.5), T).unwrap();
206        ogeom_algo::attach_pcurve(
207            &mut model,
208            &edge,
209            off.into(),
210            surface,
211            ogeom_topo::Location::identity(),
212            (0.0, 10.0),
213        )
214        .unwrap();
215
216        let report = repair_same_parameter(&mut model, &edge, T).unwrap();
217        assert_eq!(report.widened, 1);
218        let data = model.node(&edge).unwrap().data().as_edge().unwrap();
219        assert!(data.same_parameter());
220        assert!(
221            data.tolerance.get() >= 0.5,
222            "the tolerance covers the measured gap, got {}",
223            data.tolerance.get()
224        );
225    }
226}