1use ogeom_algo::{
14 Built, History, is_shell_closed, make_edge_between, make_face_on, make_shell, make_solid,
15 make_vertex, make_wire,
16};
17use ogeom_core::FastMap;
18use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
19use ogeom_geom::Curve2d as _;
20use ogeom_geom::Curve3d as _;
21use ogeom_geom::Surface as _;
22use ogeom_geom::{Curve, PlanarCurve, SurfaceGeometry};
23use ogeom_math::Point;
24use ogeom_topo::{
25 EdgeRepr, Filter, Location, Model, NodeData, Shape, ShapeType, TShapeId, explore,
26 explore_unique,
27};
28
29struct Broken {
32 face: Shape,
33 surface_id: ogeom_topo::SurfaceId,
34 surface: SurfaceGeometry,
35 rings: Vec<(Shape, Shape, Curve, (f64, f64))>,
38}
39
40struct Chain {
42 point: Point,
43 dir: ogeom_math::Vector,
44 members: Vec<usize>,
45}
46
47pub fn reanchor_periodic_rings(
61 model: &mut Model,
62 shape: &Shape,
63 tol: Tolerances,
64) -> OgeomResult<(Built, usize)> {
65 let (rings, moved) = reanchor_rings(model, shape, tol)?;
66 let (seamed, count) = match crate::seam_periodic_faces(model, &rings.shape, tol) {
68 Ok(done) => done,
69 Err(e @ ogeom_core::OgeomError::Cancelled) => return Err(e),
70 Err(_) => return Ok((rings, moved)),
71 };
72 if count == 0 {
73 return Ok((rings, moved));
74 }
75 let history = rings.history.then(&seamed.history);
76 Ok((Built::new(seamed.shape, history), moved + count))
77}
78
79fn reanchor_rings(
81 model: &mut Model,
82 shape: &Shape,
83 tol: Tolerances,
84) -> OgeomResult<(Built, usize)> {
85 if model.kind_of(shape)? != ShapeType::Solid {
86 ogeom_bail!(Construction, "ring re-anchoring heals solids");
87 }
88
89 let mut revolution: Vec<Broken> = Vec::new();
91 let mut any_broken = false;
92 for face in explore(model, shape, Filter::OfType(ShapeType::Face))? {
93 let Some(node) = model.node(&face) else {
94 ogeom_bail!(Dangling, "face is not in this model");
95 };
96 let NodeData::Face(data) = node.data() else {
97 ogeom_bail!(Construction, "face node holds no face data");
98 };
99 let Some(surface) = model.geometry().surface(data.surface) else {
100 ogeom_bail!(Dangling, "face refers to a surface not in this model");
101 };
102 if !surface.is_periodic_u() {
103 continue;
104 }
105 let surface = surface.clone();
106 let surface_id = data.surface;
107 let wires = explore(model, &face, Filter::OfType(ShapeType::Wire))?;
108
109 let mut ring_edges: Vec<Shape> = Vec::new();
113 let mut seen_twice: Vec<TShapeId> = Vec::new();
114 for wire in &wires {
115 let edges = explore(model, wire, Filter::OfType(ShapeType::Edge))?;
116 for edge in &edges {
117 if edges.iter().filter(|e| e.node() == edge.node()).count() == 2 {
118 if !seen_twice.contains(&edge.node()) {
119 seen_twice.push(edge.node());
120 }
121 continue;
122 }
123 let Some((a, b)) = ogeom_algo::edge_vertices(model, edge)? else {
124 continue;
125 };
126 if a.is_same(&b) && !ring_edges.iter().any(|r| r.node() == edge.node()) {
127 ring_edges.push(edge.clone());
128 }
129 }
130 }
131 if ring_edges.len() != 2 {
132 continue;
133 }
134 let broken_here = wires.len() == 2 && seen_twice.is_empty();
135 any_broken |= broken_here;
136 let mut rings = Vec::new();
137 for edge in ring_edges {
138 let Some((a, _)) = ogeom_algo::edge_vertices(model, &edge)? else {
139 continue;
140 };
141 let Some(edge_node) = model.node(&edge) else {
142 continue;
143 };
144 let Some(edge_data) = edge_node.data().as_edge() else {
145 continue;
146 };
147 let Some(EdgeRepr::Curve3d { curve, range, .. }) = edge_data.curve3d() else {
148 continue;
149 };
150 let Some(geometry) = model.geometry().curve(*curve) else {
151 continue;
152 };
153 rings.push((edge, a, geometry.clone(), *range));
154 }
155 if rings.len() != 2 {
156 continue;
157 }
158 revolution.push(Broken {
159 face,
160 surface_id,
161 surface,
162 rings,
163 });
164 }
165 if !any_broken {
166 return Ok((Built::new(shape.clone(), History::new()), 0));
167 }
168 let broken = revolution;
169
170 let mut history = History::new();
183 let axis_of = |s: &SurfaceGeometry| -> Option<(Point, ogeom_math::Vector)> {
184 match s {
185 SurfaceGeometry::Cylinder(c) => {
186 let f = c.cylinder().frame();
187 Some((f.origin(), f.z().vector()))
188 }
189 SurfaceGeometry::Cone(c) => {
190 let f = c.cone().frame();
191 Some((f.origin(), f.z().vector()))
192 }
193 SurfaceGeometry::Sphere(c) => {
194 let f = c.sphere().frame();
195 Some((f.origin(), f.z().vector()))
196 }
197 SurfaceGeometry::Torus(c) => {
198 let f = c.torus().frame();
199 Some((f.origin(), f.z().vector()))
200 }
201 _ => None,
202 }
203 };
204 let mut chains: Vec<Chain> = Vec::new();
205 for (i, b) in broken.iter().enumerate() {
206 let Some((point, dir)) = axis_of(&b.surface) else {
207 continue;
208 };
209 let joined = chains.iter_mut().find(|c| {
210 c.dir.cross(dir).magnitude() < 1e-6
211 && (point - c.point).cross(c.dir).magnitude() < tol.confusion() * 1e3
212 });
213 match joined {
214 Some(c) => c.members.push(i),
215 None => chains.push(Chain {
216 point,
217 dir,
218 members: vec![i],
219 }),
220 }
221 }
222
223 let mut substitution: FastMap<TShapeId, Shape> = FastMap::default();
224 let mut healable: Vec<usize> = Vec::new();
225 for chain in &chains {
226 let anchored = anchor_chain(model, &broken, chain, tol);
227 match anchored {
228 Ok(subs) => {
229 for (node, edge, old_edge, old_vertex) in subs {
230 history.modify(&old_edge, edge.clone());
231 history.delete(&old_vertex);
232 substitution.insert(node, edge);
233 }
234 healable.extend_from_slice(&chain.members);
235 }
236 Err(_) => {
237 }
239 }
240 }
241 if healable.is_empty() {
242 return Ok((Built::new(shape.clone(), History::new()), 0));
243 }
244
245 let mut face_map: FastMap<TShapeId, Shape> = FastMap::default();
247 for face in explore(model, shape, Filter::OfType(ShapeType::Face))? {
248 let uses_any = explore_unique(model, &face, ShapeType::Edge)?
254 .iter()
255 .any(|e| substitution.contains_key(&e.node()));
256 let is_revolution = healable.iter().any(|&i| broken[i].face.is_same(&face));
257 if !uses_any && !is_revolution {
258 continue;
259 }
260 let rebuilt = if let Some(b) = healable
261 .iter()
262 .map(|&i| &broken[i])
263 .find(|b| b.face.is_same(&face))
264 {
265 rebuild_broken_face(
266 model,
267 b.surface_id,
268 &b.surface,
269 &b.rings,
270 &substitution,
271 tol,
272 )?
273 } else {
274 rebuild_plain_face(model, &face, &substitution, tol)?
275 };
276 let oriented = if face.orientation() == ogeom_topo::Orientation::Reversed {
280 rebuilt.reversed()
281 } else {
282 rebuilt.clone()
283 };
284 history.modify(&face, oriented);
285 face_map.insert(face.node(), rebuilt);
286 }
287
288 let mut shells = Vec::new();
290 for shell in explore_unique(model, shape, ShapeType::Shell)? {
291 let faces: Vec<Shape> = explore(model, &shell, Filter::OfType(ShapeType::Face))?
292 .into_iter()
293 .map(|f| {
294 face_map.get(&f.node()).map_or(f.clone(), |n| {
295 if f.orientation() == ogeom_topo::Orientation::Reversed {
296 n.reversed()
297 } else {
298 n.clone()
299 }
300 })
301 })
302 .collect();
303 let rebuilt = make_shell(model, &faces)?.shape;
304 if !is_shell_closed(model, &rebuilt)? {
305 ogeom_bail!(
306 Construction,
307 "re-anchoring left the shell open; the shape resists this \
308 repair"
309 );
310 }
311 history.modify(&shell, rebuilt.clone());
312 shells.push(rebuilt);
313 }
314 let solid = make_solid(model, &shells)?.shape;
315 history.modify(shape, solid.clone());
316 let moved = substitution.len();
317 Ok((Built::new(solid, history), moved))
318}
319
320#[allow(clippy::type_complexity)]
327fn anchor_chain(
328 model: &mut Model,
329 broken: &[Broken],
330 chain: &Chain,
331 tol: Tolerances,
332) -> OgeomResult<Vec<(TShapeId, Shape, Shape, Shape)>> {
333 let (axis_point, axis_dir) = (chain.point, chain.dir);
334 let radial = {
335 let (_, vertex, curve, _) = &broken[chain.members[0]].rings[0];
336 let Curve::Circle(c) = curve else {
337 ogeom_bail!(
338 Construction,
339 "a ring to re-anchor is not a circle; the repair does not \
340 know its parameterization"
341 );
342 };
343 let circle = c.circle();
344 let Some(node) = model.node(vertex) else {
345 ogeom_bail!(Dangling, "vertex is not in this model");
346 };
347 let Some(data) = node.data().as_vertex() else {
348 ogeom_bail!(Construction, "vertex node holds no vertex data");
349 };
350 let r = data.point - circle.centre();
351 let radial = r - axis_dir * r.dot(axis_dir);
352 if radial.magnitude() <= tol.confusion() {
353 ogeom_bail!(Construction, "an anchor vertex sits on the axis");
354 }
355 radial / radial.magnitude()
356 };
357
358 let mut out = Vec::new();
359 let mut done: Vec<TShapeId> = Vec::new();
360 for &i in &chain.members {
361 for (old_edge, old_vertex, curve, _) in &broken[i].rings {
362 if done.contains(&old_edge.node()) {
363 continue;
364 }
365 done.push(old_edge.node());
366 let Curve::Circle(c) = curve else {
367 ogeom_bail!(
368 Construction,
369 "a ring to re-anchor is not a circle; the repair does \
370 not know its parameterization"
371 );
372 };
373 let circle = c.circle();
374 if circle.frame().z().vector().cross(axis_dir).magnitude() > 1e-6
376 || (circle.centre() - axis_point).cross(axis_dir).magnitude()
377 > tol.confusion() * 1e3
378 {
379 ogeom_bail!(
380 Construction,
381 "the rings are not coaxial; the half-plane repair does \
382 not apply"
383 );
384 }
385 let target = circle.centre() + radial * circle.radius();
386 let current = {
387 let Some(node) = model.node(old_vertex) else {
388 ogeom_bail!(Dangling, "vertex is not in this model");
389 };
390 let Some(data) = node.data().as_vertex() else {
391 ogeom_bail!(Construction, "vertex node holds no vertex data");
392 };
393 data.point
394 };
395 if current.distance(target) <= tol.confusion() * 1e2 {
396 continue;
397 }
398 let Some(t_star) = circle_parameter(curve, target) else {
399 ogeom_bail!(Construction, "an anchor point fell off its circle");
400 };
401 let period = {
402 let (lo, hi) = curve.domain();
403 hi - lo
404 };
405 let vertex = make_vertex(model, target).shape;
406 let rebuilt = make_edge_between(
407 model,
408 curve.clone(),
409 (t_star, t_star + period),
410 &vertex,
411 &vertex,
412 tol,
413 )?
414 .shape;
415 out.push((
416 old_edge.node(),
417 rebuilt,
418 old_edge.clone(),
419 old_vertex.clone(),
420 ));
421 }
422 }
423 Ok(out)
424}
425
426pub(crate) fn circle_parameter(curve: &Curve, p: Point) -> Option<f64> {
428 let Curve::Circle(c) = curve else {
429 return None;
430 };
431 let local = c.circle().frame().to_local(p);
432 Some(local.y.atan2(local.x).rem_euclid(core::f64::consts::TAU))
433}
434
435#[allow(clippy::type_complexity)]
439fn rebuild_broken_face(
440 model: &mut Model,
441 _old_surface_id: ogeom_topo::SurfaceId,
442 surface: &SurfaceGeometry,
443 rings: &[(Shape, Shape, Curve, (f64, f64))],
444 substitution: &FastMap<TShapeId, Shape>,
445 tol: Tolerances,
446) -> OgeomResult<Shape> {
447 let resolved: Vec<Shape> = rings
448 .iter()
449 .map(|(e, ..)| substitution.get(&e.node()).unwrap_or(e).clone())
450 .collect();
451 ogeom_algo::make_revolution_band(model, surface, &resolved[0], &resolved[1], tol)
452}
453
454pub(crate) fn rebuild_plain_face(
456 model: &mut Model,
457 face: &Shape,
458 substitution: &FastMap<TShapeId, Shape>,
459 tol: Tolerances,
460) -> OgeomResult<Shape> {
461 let (surface_id, surface) = {
462 let Some(node) = model.node(face) else {
463 ogeom_bail!(Dangling, "face is not in this model");
464 };
465 let NodeData::Face(data) = node.data() else {
466 ogeom_bail!(Construction, "face node holds no face data");
467 };
468 let Some(surface) = model.geometry().surface(data.surface) else {
469 ogeom_bail!(Dangling, "face refers to a surface not in this model");
470 };
471 (data.surface, surface.clone())
472 };
473 let mut wires = Vec::new();
474 let mut replaced = Vec::new();
475 for wire in model.ordered_children_of(&face.oriented(ogeom_topo::Orientation::Forward))? {
478 let mut edges = Vec::new();
479 for edge in model.ordered_children_of(&wire)? {
480 match substitution.get(&edge.node()) {
481 Some(new_edge) => {
482 let placed = if edge.orientation() == ogeom_topo::Orientation::Reversed {
483 new_edge.reversed()
484 } else {
485 new_edge.clone()
486 };
487 replaced.push(new_edge.clone());
488 edges.push(placed);
489 }
490 None => edges.push(edge),
491 }
492 }
493 wires.push(make_wire(model, &edges, tol)?.shape);
494 }
495 attach_face_pcurves(model, &surface, surface_id, &replaced, tol)?;
496 Ok(make_face_on(model, surface_id, &wires, tol)?.shape)
497}
498
499pub(crate) fn attach_face_pcurves(
502 model: &mut Model,
503 surface: &SurfaceGeometry,
504 surface_id: ogeom_topo::SurfaceId,
505 edges: &[Shape],
506 tol: Tolerances,
507) -> OgeomResult<()> {
508 for edge in edges {
509 let (curve, range, already) = {
510 let Some(node) = model.node(edge) else {
511 ogeom_bail!(Dangling, "edge is not in this model");
512 };
513 let Some(data) = node.data().as_edge() else {
514 ogeom_bail!(Construction, "edge node holds no edge data");
515 };
516 let already = data
517 .representations
518 .iter()
519 .any(|r| matches!(r, EdgeRepr::PCurve { surface: s, .. } if *s == surface_id));
520 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
521 continue;
522 };
523 let Some(geometry) = model.geometry().curve(*curve) else {
524 ogeom_bail!(Dangling, "curve is not in this model");
525 };
526 (geometry.clone(), *range, already)
527 };
528 if already {
529 continue;
530 }
531 let Some(pcurve) = ogeom_intersect::exact_pcurve_over(&curve, range, surface, tol) else {
532 continue;
533 };
534 let pcurve = if let PlanarCurve::Line(l) = &pcurve {
537 let (lo, hi) = (l.domain().0.min(range.0), l.domain().1.max(range.1));
538 ogeom_geom::Line2d::over(l.axis(), lo, hi)
539 .map(Into::into)
540 .unwrap_or(pcurve)
541 } else {
542 pcurve
543 };
544 ogeom_algo::attach_pcurve(model, edge, pcurve, surface_id, Location::identity(), range)?;
545 }
546 Ok(())
547}