1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
7use ogeom_geom::Transformable as _;
8use ogeom_topo::{EdgeRepr, Model, NodeData, Shape, ShapeType};
9
10#[derive(Debug, Default)]
12pub struct FixedTrims {
13 pub fitted: usize,
15 pub already: usize,
17 pub worst: f64,
20 pub refused: Vec<(Shape, f64)>,
23}
24
25pub fn fix_face_pcurves(
40 model: &mut Model,
41 face: &Shape,
42 cap: f64,
43 tol: Tolerances,
44) -> OgeomResult<FixedTrims> {
45 if model.kind_of(face)? != ShapeType::Face {
46 ogeom_bail!(Construction, "fix_face_pcurves fixes a face");
47 }
48 let (surface_id, surface) = {
49 let Some(node) = model.node(face) else {
50 ogeom_bail!(Dangling, "face is not in this model");
51 };
52 let NodeData::Face(data) = node.data() else {
53 ogeom_bail!(Construction, "face node holds no face data");
54 };
55 let Some(stored) = model.geometry().surface(data.surface) else {
56 ogeom_bail!(Dangling, "face refers to a surface not in this model");
57 };
58 let placement = face.transform(model.datums())?;
59 (data.surface, stored.transformed(&placement, tol)?)
60 };
61
62 let mut report = FixedTrims::default();
63 for wire in model.ordered_children_of(face)? {
64 for edge in model.ordered_children_of(&wire)? {
65 let (curve, range) = {
66 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
67 continue;
68 };
69 if data.pcurve_for(surface_id, edge.location()).is_some() {
70 report.already += 1;
71 continue;
72 }
73 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
74 ogeom_bail!(
75 Construction,
76 "an edge has no space curve; nothing can be projected"
77 );
78 };
79 let Some(geometry) = model.geometry().curve(*curve) else {
80 ogeom_bail!(Dangling, "an edge names a curve not in this model");
81 };
82 let placed = edge.transform(model.datums())?;
83 (geometry.clone().transformed(&placed, tol)?, *range)
84 };
85 match ogeom_algo::pcurve_fit::fit_projected_pcurve_capped(
86 &curve, range, &surface, cap, tol,
87 ) {
88 Ok((pcurve, _, _, worst_off, _)) => {
89 report.fitted += 1;
90 report.worst = report.worst.max(worst_off);
91 if worst_off > tol.confusion()
92 && let Some(node) = model.node_mut(&edge)
93 && let NodeData::Edge(data) = node.data_mut()
94 {
95 data.tolerance = data.tolerance.widen_to(worst_off + tol.confusion());
96 }
97 ogeom_algo::attach_pcurve(
98 model,
99 &edge,
100 pcurve,
101 surface_id,
102 ogeom_topo::Location::identity(),
103 range,
104 )?;
105 }
106 Err(refusal) => {
107 let off = refusal
110 .to_string()
111 .split_whitespace()
112 .find_map(|w| w.parse::<f64>().ok())
113 .unwrap_or(f64::INFINITY);
114 report.refused.push((edge.clone(), off));
115 }
116 }
117 }
118 }
119 Ok(report)
120}
121
122#[derive(Debug, Default)]
124pub struct ReanchoredBoundaries {
125 pub moved: usize,
127 pub worst_before: f64,
129 pub worst_after: f64,
132 pub refused: Vec<(Shape, f64)>,
134}
135
136pub fn reanchor_boundaries(
156 model: &mut Model,
157 shape: &Shape,
158 cap: f64,
159 tol: Tolerances,
160) -> OgeomResult<(ogeom_algo::Built, ReanchoredBoundaries)> {
161 use ogeom_topo::{Filter, explore};
162 let mut report = ReanchoredBoundaries::default();
163 let mut reshape = crate::reshape::Reshape::new();
164 let mut done: std::collections::HashSet<ogeom_topo::TShapeId> =
165 std::collections::HashSet::new();
166
167 const SAMPLES: usize = 33;
168 for face in explore(model, shape, Filter::OfType(ShapeType::Face))? {
169 let surface = {
170 let Some(data) = model.node(&face).and_then(|n| n.data().as_face()) else {
171 continue;
172 };
173 let Some(stored) = model.geometry().surface(data.surface) else {
174 continue;
175 };
176 let placement = face.transform(model.datums())?;
177 stored.clone().transformed(&placement, tol)?
178 };
179 for edge in explore(model, &face, Filter::OfType(ShapeType::Edge))? {
180 if !done.insert(edge.node()) {
181 continue;
182 }
183 let (curve, range, reprs) = {
184 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
185 continue;
186 };
187 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
188 continue;
189 };
190 let Some(geometry) = model.geometry().curve(*curve) else {
191 continue;
192 };
193 let placed = edge.transform(model.datums())?;
194 (
195 geometry.clone().transformed(&placed, tol)?,
196 *range,
197 data.representations.clone(),
198 )
199 };
200 use ogeom_geom::Curve3d as _;
202 use ogeom_geom::Surface as _;
203 let mut params = Vec::with_capacity(SAMPLES);
204 let mut projected = Vec::with_capacity(SAMPLES);
205 let mut worst = 0.0_f64;
206 let mut seed: Option<(f64, f64)> = None;
207 for i in 0..SAMPLES {
208 #[allow(clippy::cast_precision_loss, reason = "a sample index")]
209 let t = range.0 + (range.1 - range.0) * i as f64 / (SAMPLES - 1) as f64;
210 let p = curve.point_at(t, tol)?;
211 let hit = match seed {
212 Some(uv) => ogeom_algo::project_on_surface_from(&surface, p, uv, tol)
213 .or_else(|_| ogeom_algo::project_on_surface(&surface, p, 24, tol))?,
214 None => ogeom_algo::project_on_surface(&surface, p, 24, tol)?,
215 };
216 seed = Some(hit.parameters);
217 worst = worst.max(hit.distance);
218 params.push(t);
219 projected.push(surface.point_at(hit.parameters.0, hit.parameters.1, tol)?);
220 }
221 if worst <= tol.confusion() * 1e3 {
222 continue; }
224 report.worst_before = report.worst_before.max(worst);
225 if worst > cap {
226 report.refused.push((edge.clone(), worst));
227 continue;
228 }
229
230 let fitted = ogeom_geom::fit::fit_points_at(
231 ¶ms,
232 &projected,
233 3,
234 (tol.confusion() * 1e3).max(worst * 1e-3),
235 tol,
236 )?;
237 report.worst_after = report.worst_after.max(fitted.error);
238
239 let bounds = model.children_of(&edge)?;
246 let (Some(va), Some(vb)) = (bounds.first().cloned(), bounds.last().cloned()) else {
247 continue;
248 };
249 for v in [&va, &vb] {
250 if let Some(node) = model.node_mut(v)
251 && let NodeData::Vertex(data) = node.data_mut()
252 {
253 data.tolerance = data.tolerance.widen_to(worst + tol.confusion());
254 }
255 }
256 let rebuilt = ogeom_algo::make_edge_between(
257 model,
258 ogeom_geom::Curve::BSpline(fitted.curve),
259 (range.0, range.1),
260 &va,
261 &vb,
262 tol,
263 )?
264 .shape;
265 if let Some(node) = model.node_mut(&rebuilt)
266 && let NodeData::Edge(data) = node.data_mut()
267 {
268 data.tolerance = data.tolerance.widen_to(worst + tol.confusion());
269 for repr in &reprs {
273 if !matches!(repr, EdgeRepr::Curve3d { .. }) {
274 data.add(repr.clone());
275 }
276 }
277 }
278 reshape.replace(&edge, rebuilt);
279 report.moved += 1;
280 }
281 }
282 if reshape.is_empty() {
283 return Ok((ogeom_algo::Built::from_nothing(shape.clone()), report));
284 }
285 let built = reshape.apply(model, shape)?;
286 Ok((built, report))
287}