1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
4use ogeom_geom::Transformable as _;
5use ogeom_topo::{EdgeRepr, Model, NodeData, Shape, ShapeType};
6
7#[derive(Debug, Default)]
9pub struct FixedTrims {
10 pub fitted: usize,
12 pub already: usize,
14 pub worst: f64,
17 pub refused: Vec<(Shape, f64)>,
20 pub recharted: bool,
25}
26
27pub fn fix_face_pcurves(
52 model: &mut Model,
53 face: &Shape,
54 cap: f64,
55 tol: Tolerances,
56) -> OgeomResult<FixedTrims> {
57 let plan = plan_trims(model, face, tol)?;
58 let fits = fit_trims(std::slice::from_ref(&plan), cap, tol);
59 let mut report = FixedTrims::default();
60 for (plan, fits) in [plan].into_iter().zip(fits) {
61 attach_trims(model, plan, fits, tol, &mut report)?;
62 }
63 if report.refused.is_empty() {
64 match ogeom_bool::rechart_clear_of_poles(model, face, tol)? {
65 ogeom_bool::Recharted::Untouched => {}
66 ogeom_bool::Recharted::Restated => report.recharted = true,
67 ogeom_bool::Recharted::Refused(why) => {
68 ogeom_bail!(NotDone, "{why}");
69 }
70 }
71 }
72 Ok(report)
73}
74
75pub(crate) struct TrimPlan {
78 surface_id: ogeom_topo::SurfaceId,
79 surface: Option<ogeom_geom::SurfaceGeometry>,
82 uses: Vec<(Shape, Option<Projected>)>,
83}
84
85type Projected = (ogeom_geom::Curve, (f64, f64));
87
88pub(crate) enum TrimFit {
92 Fitted {
93 pcurve: ogeom_geom::PlanarCurve,
94 worst_off: f64,
95 off: f64,
96 },
97 Refused(f64),
98}
99
100pub(crate) fn plan_trims(
108 model: &mut Model,
109 face: &Shape,
110 tol: Tolerances,
111) -> OgeomResult<TrimPlan> {
112 if model.kind_of(face)? != ShapeType::Face {
113 ogeom_bail!(Construction, "fix_face_pcurves fixes a face");
114 }
115 model.unshare(face)?;
116 let surface_id = {
117 let Some(node) = model.node(face) else {
118 ogeom_bail!(Dangling, "face is not in this model");
119 };
120 let NodeData::Face(data) = node.data() else {
121 ogeom_bail!(Construction, "face node holds no face data");
122 };
123 if model.geometry().surface(data.surface).is_none() {
124 ogeom_bail!(Dangling, "face refers to a surface not in this model");
125 }
126 data.surface
127 };
128 let mut uses = Vec::new();
129 for wire in model.ordered_children_of(face)? {
130 for edge in model.ordered_children_of(&wire)? {
131 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
132 continue;
133 };
134 if data.pcurve_for(surface_id, edge.location()).is_some() {
135 uses.push((edge, None));
136 continue;
137 }
138 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
139 ogeom_bail!(
140 Construction,
141 "an edge has no space curve; nothing can be projected"
142 );
143 };
144 let Some(geometry) = model.geometry().curve(*curve) else {
145 ogeom_bail!(Dangling, "an edge names a curve not in this model");
146 };
147 let placed = edge.transform(model.datums())?;
148 let job = (geometry.clone().transformed(&placed, tol)?, *range);
149 uses.push((edge, Some(job)));
150 }
151 }
152 let surface = if uses.iter().any(|(_, job)| job.is_some()) {
153 let placement = face.transform(model.datums())?;
154 model
155 .geometry()
156 .surface(surface_id)
157 .map(|stored| stored.transformed(&placement, tol))
158 .transpose()?
159 } else {
160 None
161 };
162 Ok(TrimPlan {
163 surface_id,
164 surface,
165 uses,
166 })
167}
168
169pub(crate) fn fit_trims(
172 plans: &[TrimPlan],
173 cap: f64,
174 tol: Tolerances,
175) -> Vec<Vec<Option<OgeomResult<TrimFit>>>> {
176 let jobs: Vec<(usize, usize)> = plans
177 .iter()
178 .enumerate()
179 .flat_map(|(p, plan)| {
180 plan.uses
181 .iter()
182 .enumerate()
183 .filter(|(_, (_, job))| job.is_some())
184 .map(move |(u, _)| (p, u))
185 })
186 .collect();
187 let fitted = ogeom_core::parallel::map_ordered(&jobs, |_, &(p, u)| {
188 let plan = &plans[p];
189 let (Some((curve, range)), Some(surface)) = (&plan.uses[u].1, &plan.surface) else {
190 ogeom_bail!(Construction, "a planned trim has nothing to fit");
191 };
192 fit_trim(curve, *range, surface, cap, tol)
193 });
194 let mut out: Vec<Vec<Option<OgeomResult<TrimFit>>>> = plans
195 .iter()
196 .map(|plan| plan.uses.iter().map(|_| None).collect())
197 .collect();
198 for ((p, u), fit) in jobs.into_iter().zip(fitted) {
199 out[p][u] = Some(fit);
200 }
201 out
202}
203
204fn fit_trim(
206 curve: &ogeom_geom::Curve,
207 range: (f64, f64),
208 surface: &ogeom_geom::SurfaceGeometry,
209 cap: f64,
210 tol: Tolerances,
211) -> OgeomResult<TrimFit> {
212 use ogeom_algo::pcurve_fit::CappedFit;
213 match ogeom_algo::pcurve_fit::fit_projected_pcurve_within(curve, range, surface, cap, tol) {
214 Ok(CappedFit::Fitted((pcurve, _, _, worst_off, _))) => {
215 let gap = ogeom_algo::pcurve_fit::lifted_gap(
219 (curve, range),
220 (&pcurve, range),
221 surface,
222 false,
223 tol,
224 )?;
225 Ok(TrimFit::Fitted {
226 pcurve,
227 worst_off,
228 off: gap.max(worst_off),
229 })
230 }
231 Ok(CappedFit::TooFar(off)) => Ok(TrimFit::Refused(off)),
232 Err(_) => Ok(TrimFit::Refused(f64::INFINITY)),
234 }
235}
236
237pub(crate) fn attach_trims(
246 model: &mut Model,
247 plan: TrimPlan,
248 fits: Vec<Option<OgeomResult<TrimFit>>>,
249 tol: Tolerances,
250 report: &mut FixedTrims,
251) -> OgeomResult<()> {
252 for ((edge, job), fit) in plan.uses.into_iter().zip(fits) {
253 let attached = model
254 .node(&edge)
255 .and_then(|n| n.data().as_edge())
256 .is_some_and(|data| data.pcurve_for(plan.surface_id, edge.location()).is_some());
257 let (Some((_, range)), Some(fit), false) = (job, fit, attached) else {
258 report.already += 1;
259 continue;
260 };
261 match fit? {
262 TrimFit::Fitted {
263 pcurve,
264 worst_off,
265 off,
266 } => {
267 report.fitted += 1;
268 report.worst = report.worst.max(worst_off);
269 if off > tol.confusion() {
272 model.widen(&edge, ogeom_core::Tolerance::new(off + tol.confusion())?)?;
273 }
274 ogeom_algo::attach_pcurve(
275 model,
276 &edge,
277 pcurve,
278 plan.surface_id,
279 ogeom_topo::Location::identity(),
280 range,
281 )?;
282 }
283 TrimFit::Refused(off) => report.refused.push((edge, off)),
284 }
285 }
286 Ok(())
287}
288
289#[derive(Debug, Default)]
291pub struct ReanchoredBoundaries {
292 pub moved: usize,
294 pub worst_before: f64,
296 pub worst_after: f64,
299 pub refused: Vec<(Shape, f64)>,
301}
302
303pub fn reanchor_boundaries(
325 model: &mut Model,
326 shape: &Shape,
327 cap: f64,
328 tol: Tolerances,
329) -> OgeomResult<(ogeom_algo::Built, ReanchoredBoundaries)> {
330 ogeom_algo::on_own_nodes_with(
331 model,
332 shape,
333 &[],
334 |model, shape, _| reanchor_boundaries_own(model, shape, cap, tol),
335 |(built, _)| &mut built.history,
336 )
337}
338
339fn reanchor_boundaries_own(
341 model: &mut Model,
342 shape: &Shape,
343 cap: f64,
344 tol: Tolerances,
345) -> OgeomResult<(ogeom_algo::Built, ReanchoredBoundaries)> {
346 use ogeom_topo::{Filter, explore};
347 let mut report = ReanchoredBoundaries::default();
348 let mut reshape = crate::reshape::Reshape::new();
349 let mut done: ogeom_core::FastSet<ogeom_topo::TShapeId> = ogeom_core::FastSet::default();
350
351 const SAMPLES: usize = 33;
353 for face in explore(model, shape, Filter::OfType(ShapeType::Face))? {
354 let surface = {
355 let Some(data) = model.node(&face).and_then(|n| n.data().as_face()) else {
356 continue;
357 };
358 let Some(stored) = model.geometry().surface(data.surface) else {
359 continue;
360 };
361 let placement = face.transform(model.datums())?;
362 stored.clone().transformed(&placement, tol)?
363 };
364 for edge in explore(model, &face, Filter::OfType(ShapeType::Edge))? {
365 if !done.insert(edge.node()) {
366 continue;
367 }
368 let (curve, range, reprs) = {
369 let Some(data) = model.node(&edge).and_then(|n| n.data().as_edge()) else {
370 continue;
371 };
372 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
373 continue;
374 };
375 let Some(geometry) = model.geometry().curve(*curve) else {
376 continue;
377 };
378 let placed = edge.transform(model.datums())?;
379 (
380 geometry.clone().transformed(&placed, tol)?,
381 *range,
382 data.representations.clone(),
383 )
384 };
385 use ogeom_geom::Curve3d as _;
389 use ogeom_geom::Surface as _;
390 let stations = ogeom_algo::traced::stations(&curve, range, SAMPLES - 1);
391 let mut landed: Vec<(f64, (f64, f64), f64)> = Vec::with_capacity(stations.len());
395 let mut project = |t: f64| -> OgeomResult<(ogeom_math::Point, f64)> {
396 let p = curve.point_at(t, tol)?;
397 let at = landed.partition_point(|l| l.0 < t);
398 let seed = at.checked_sub(1).or((!landed.is_empty()).then_some(0));
399 let hit = match seed.map(|i| landed[i].1) {
400 Some(uv) => ogeom_algo::project_on_surface_from(&surface, p, uv, tol)
401 .or_else(|_| ogeom_algo::project_on_surface(&surface, p, 24, tol))?,
402 None => ogeom_algo::project_on_surface(&surface, p, 24, tol)?,
403 };
404 landed.insert(at, (t, hit.parameters, hit.distance));
405 Ok((
406 surface.point_at(hit.parameters.0, hit.parameters.1, tol)?,
407 hit.distance,
408 ))
409 };
410 let mut worst = 0.0_f64;
411 let mut shadow: ogeom_core::FastMap<u64, ogeom_math::Point> =
412 ogeom_core::FastMap::with_capacity_and_hasher(stations.len(), Default::default());
413 for &t in &stations {
414 let (q, off) = project(t)?;
415 worst = worst.max(off);
416 shadow.insert(t.to_bits(), q);
417 }
418 if worst <= tol.confusion() * 1e3 {
419 continue; }
421 report.worst_before = report.worst_before.max(worst);
422 if worst > cap {
423 report.refused.push((edge.clone(), worst));
424 continue;
425 }
426
427 let target = (tol.confusion() * 1e3).max(worst * 1e-3);
431 let fitted = ogeom_algo::traced::fit_traced(
432 |t| match shadow.get(&t.to_bits()) {
433 Some(q) => Ok(*q),
434 None => {
435 let (q, off) = project(t)?;
436 worst = worst.max(off);
437 Ok(q)
438 }
439 },
440 &stations,
441 3,
442 target,
443 tol,
444 )?;
445 report.worst_before = report.worst_before.max(worst);
446 report.worst_after = report.worst_after.max(fitted.error);
447 let moved_by = worst + fitted.error + tol.confusion();
448
449 let bounds = model.children_of(&edge)?;
456 let (Some(va), Some(vb)) = (bounds.first().cloned(), bounds.last().cloned()) else {
457 continue;
458 };
459 for v in [&va, &vb] {
460 if let Some(node) = model.node_mut(v)
461 && let NodeData::Vertex(data) = node.data_mut()
462 {
463 data.tolerance = data.tolerance.widen_to(moved_by);
464 }
465 }
466 let rebuilt = ogeom_algo::make_edge_between(
467 model,
468 ogeom_geom::Curve::BSpline(fitted.curve),
469 (range.0, range.1),
470 &va,
471 &vb,
472 tol,
473 )?
474 .shape;
475 if let Some(node) = model.node_mut(&rebuilt)
476 && let NodeData::Edge(data) = node.data_mut()
477 {
478 data.tolerance = data.tolerance.widen_to(moved_by);
479 for repr in &reprs {
483 if !matches!(repr, EdgeRepr::Curve3d { .. }) {
484 data.add(repr.clone());
485 }
486 }
487 }
488 ogeom_algo::state_pcurve_gaps_of(model, core::slice::from_ref(&rebuilt), tol)?;
491 reshape.replace(&edge, rebuilt);
492 report.moved += 1;
493 }
494 }
495 if reshape.is_empty() {
496 return Ok((ogeom_algo::Built::from_nothing(shape.clone()), report));
497 }
498 let built = reshape.apply(model, shape)?;
499 Ok((built, report))
500}