1use ogeom_core::{FastSet, OgeomResult, Tolerance, Tolerances};
14use ogeom_topo::{EdgeRepr, Filter, Model, NodeData, Shape, ShapeType, explore};
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
18pub struct SameParameterReport {
19 pub checked: usize,
21 pub agreed: usize,
23 pub widened: usize,
25 pub trivial: usize,
27}
28
29pub 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
92enum Found {
94 Absent,
96 Trivial { flag: bool },
99 Unmeasured,
101 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 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
144fn 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 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}