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