1use ogeom_core::{OgeomResult, Tolerances};
14use ogeom_geom::{Curve2d as _, Curve3d as _, Surface as _};
15use ogeom_topo::{EdgeRepr, Filter, Model, NodeData, Shape, ShapeType, TShapeId, explore};
16
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
19pub struct SameParameterReport {
20 pub checked: usize,
22 pub agreed: usize,
24 pub widened: usize,
26 pub trivial: usize,
28}
29
30pub fn repair_same_parameter(
44 model: &mut Model,
45 shape: &Shape,
46 tol: Tolerances,
47) -> OgeomResult<SameParameterReport> {
48 const SAMPLES: usize = 24;
49 let mut report = SameParameterReport::default();
50 let mut done: Vec<TShapeId> = Vec::new();
51 for edge in explore(model, shape, Filter::OfType(ShapeType::Edge))? {
52 if done.contains(&edge.node()) {
53 continue;
54 }
55 done.push(edge.node());
56 report.checked += 1;
57
58 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
59 continue;
60 };
61 let data = data.clone();
62 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
63 report.trivial += 1;
66 set_flag(model, &edge, true);
67 continue;
68 };
69 let Some(curve) = model.geometry().curve(*curve).cloned() else {
70 report.trivial += 1;
71 continue;
72 };
73 let (ca, cb) = *range;
74
75 let mut pairs: Vec<(
77 ogeom_geom::PlanarCurve,
78 (f64, f64),
79 ogeom_geom::SurfaceGeometry,
80 )> = Vec::new();
81 for repr in &data.representations {
82 match repr {
83 EdgeRepr::PCurve {
84 curve: pc,
85 surface,
86 range,
87 ..
88 } => {
89 if let (Some(p), Some(s)) = (
90 model.geometry().pcurve(*pc).cloned(),
91 model.geometry().surface(*surface).cloned(),
92 ) {
93 pairs.push((p, *range, s));
94 }
95 }
96 EdgeRepr::Seam {
97 forward,
98 reversed,
99 surface,
100 range,
101 ..
102 } => {
103 for pc in [forward, reversed] {
104 if let (Some(p), Some(s)) = (
105 model.geometry().pcurve(*pc).cloned(),
106 model.geometry().surface(*surface).cloned(),
107 ) {
108 pairs.push((p, *range, s));
109 }
110 }
111 }
112 EdgeRepr::Curve3d { .. } => {}
113 _ => {}
114 }
115 }
116 if pairs.is_empty() {
117 report.trivial += 1;
118 set_flag(model, &edge, true);
119 continue;
120 }
121
122 let mut worst = 0.0_f64;
123 for (pcurve, prange, surface) in &pairs {
124 for i in 0..=SAMPLES {
125 #[allow(clippy::cast_precision_loss)]
126 let t = ca + (cb - ca) * i as f64 / SAMPLES as f64;
127 let u = if (cb - ca).abs() <= f64::MIN_POSITIVE {
129 prange.0
130 } else {
131 prange.0 + (prange.1 - prange.0) * (t - ca) / (cb - ca)
132 };
133 let on_curve = curve.point_at(t, tol)?;
134 let uv = pcurve.point_at(u, tol)?;
135 let lifted = surface.point_at(uv.x, uv.y, tol)?;
136 worst = worst.max(on_curve.distance(lifted));
137 }
138 }
139
140 let within = {
141 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
142 continue;
143 };
144 data.tolerance.get()
145 };
146 if worst <= within {
147 report.agreed += 1;
148 } else {
149 report.widened += 1;
150 if let Some(node) = model.node_mut(&edge)
151 && let NodeData::Edge(data) = node.data_mut()
152 {
153 data.tolerance = data.tolerance.widen_to(worst + tol.confusion());
154 }
155 }
156 set_flag(model, &edge, true);
157 }
158 Ok(report)
159}
160
161fn set_flag(model: &mut Model, edge: &Shape, agrees: bool) {
163 if let Some(node) = model.node_mut(edge)
164 && let NodeData::Edge(data) = node.data_mut()
165 {
166 data.assert_same_parameter(agrees);
167 }
168}
169
170#[cfg(test)]
171#[allow(clippy::unwrap_used, clippy::expect_used)]
172mod tests {
173 use super::*;
174 use ogeom_math::Frame;
175
176 const T: Tolerances = Tolerances::millimetres();
177
178 fn all_flags_true(model: &Model, shape: &Shape) -> bool {
179 explore(model, shape, Filter::OfType(ShapeType::Edge))
180 .unwrap()
181 .iter()
182 .all(|e| {
183 model
184 .node(e)
185 .and_then(|n| n.data().as_edge())
186 .is_some_and(ogeom_topo::EdgeData::same_parameter)
187 })
188 }
189
190 #[test]
191 fn a_primitives_edges_agree_and_the_flag_finally_says_so() {
192 let mut model = Model::new();
193 let solid = ogeom_algo::make_cylinder(&mut model, Frame::WORLD, 2.0, 5.0, T).unwrap();
194 assert!(
195 !all_flags_true(&model, &solid.shape),
196 "the builder is honest: unverified means false"
197 );
198 let report = repair_same_parameter(&mut model, &solid.shape, T).unwrap();
199 assert_eq!(report.widened, 0, "a native primitive has nothing to widen");
200 assert!(report.agreed > 0);
201 assert!(all_flags_true(&model, &solid.shape));
202 }
203
204 #[test]
205 fn a_disagreeing_pcurve_widens_the_tolerance_into_truth() {
206 use ogeom_geom::{Line2d, LineCurve, PlaneSurface};
207 use ogeom_math::{Plane, Point, Point2};
208 let mut model = Model::new();
209 let curve: ogeom_geom::Curve =
210 LineCurve::segment(Point::new(0.0, 0.0, 0.0), Point::new(10.0, 0.0, 0.0), T)
211 .unwrap()
212 .into();
213 let edge = ogeom_algo::make_edge(&mut model, curve, (0.0, 10.0), T)
214 .unwrap()
215 .shape;
216 let surface = model.geometry_mut().add_surface(
217 PlaneSurface::over(Plane::new(Frame::WORLD), (-20.0, 20.0), (-20.0, 20.0))
218 .unwrap()
219 .into(),
220 );
221 let off = Line2d::segment(Point2::new(0.0, 0.5), Point2::new(10.0, 0.5), T).unwrap();
223 ogeom_algo::attach_pcurve(
224 &mut model,
225 &edge,
226 off.into(),
227 surface,
228 ogeom_topo::Location::identity(),
229 (0.0, 10.0),
230 )
231 .unwrap();
232
233 let report = repair_same_parameter(&mut model, &edge, T).unwrap();
234 assert_eq!(report.widened, 1);
235 let data = model.node(&edge).unwrap().data().as_edge().unwrap();
236 assert!(data.same_parameter());
237 assert!(
238 data.tolerance.get() >= 0.5,
239 "the tolerance covers the measured gap, got {}",
240 data.tolerance.get()
241 );
242 }
243}