1use ogeom_algo::{Built, History, SolidBoundary};
22use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
23use ogeom_geom::Surface as _;
24use ogeom_math::{Point, Point2, Vector};
25use ogeom_mesh::Deflection;
26use ogeom_topo::{Model, Shape, ShapeType, explore_unique};
27
28use super::{
29 FacePiece, GFace, GeneralFused, Operands, assemble_result, baked_if_scaled, general_fuse_as,
30 outward_normal,
31};
32
33pub fn self_union(model: &mut Model, solid: &Shape, tol: Tolerances) -> OgeomResult<Built> {
52 if model.kind_of(solid)? != ShapeType::Solid {
53 ogeom_bail!(Construction, "a self-union resolves a solid");
54 }
55 let crossings = ogeom_algo::self_crossings(model, solid, tol)?;
56 if crossings.is_empty() {
57 return Ok(Built::new(solid.clone(), History::new()));
58 }
59 let corners = |face: &Shape| -> OgeomResult<ogeom_core::FastSet<super::EdgeKey>> {
62 Ok(explore_unique(model, face, ShapeType::Vertex)?
63 .iter()
64 .map(super::EdgeKey::of)
65 .collect())
66 };
67 for crossing in &crossings {
68 if crossing.face.is_same(&crossing.other)
69 || !corners(&crossing.face)?.is_disjoint(&corners(&crossing.other)?)
70 {
71 ogeom_bail!(
72 NotDone,
73 "a face passes through itself or through a face it shares an edge or a \
74 corner with, which the self-union does not split"
75 );
76 }
77 }
78 let solid = &baked_if_scaled(model, solid, tol)?;
79 let fused = general_fuse_as(model, solid, solid, Operands::Itself, None, tol)?;
80 let boundary = SolidBoundary::of(model, solid, tol.confusion() * 1e4, tol)?;
81 let mut keep: Vec<Option<bool>> = vec![None; fused.pieces.len()];
82 for (index, piece) in fused.pieces.iter().enumerate() {
83 if piece.from_a && !piece.outlines.is_empty() {
84 let Some(bounds) = bounds_the_union(model, &fused, piece, &boundary, tol)? else {
85 ogeom_bail!(
86 NotDone,
87 "a piece of a face reads its two sides nowhere a step off it meets no \
88 other face"
89 );
90 };
91 keep[index] = Some(bounds);
92 }
93 }
94 for group in quiet_groups(&fused) {
95 let mut read = None;
96 for &index in &group {
97 let Some(this) = bounds_the_union(model, &fused, &fused.pieces[index], &boundary, tol)?
98 else {
99 continue;
100 };
101 if read.is_some_and(|read| read != this) {
102 ogeom_bail!(
103 NotDone,
104 "a run of faces nothing crosses bounds the union in part, which only a \
105 face meeting its edges makes"
106 );
107 }
108 read = Some(this);
109 }
110 let Some(bounds) = read else {
111 ogeom_bail!(
112 NotDone,
113 "a run of faces nothing crosses reads its two sides nowhere a step off it \
114 meets no other face"
115 );
116 };
117 for index in group {
118 keep[index] = Some(bounds);
119 }
120 }
121 let kept: Vec<(usize, bool)> = keep
122 .iter()
123 .enumerate()
124 .filter(|(_, keep)| **keep == Some(true))
125 .map(|(index, _)| (index, false))
126 .collect();
127 let built = assemble_result(model, &fused, &kept, [false, false], solid, solid, tol)?;
128 for made in explore_unique(model, &built.shape, ShapeType::Solid)? {
129 if !ogeom_algo::self_crossings(model, &made, tol)?.is_empty() {
130 ogeom_bail!(NotDone, "the union still passes through itself");
131 }
132 }
133 Ok(built)
134}
135
136fn bounds_the_union(
141 model: &Model,
142 fused: &GeneralFused,
143 piece: &FacePiece,
144 boundary: &SolidBoundary,
145 tol: Tolerances,
146) -> OgeomResult<Option<bool>> {
147 let face = &fused.a.faces[piece.face];
148 let Some((at, uv)) = interior_point(model, face, piece, tol)? else {
149 return Ok(None);
150 };
151 let normal = outward_normal(face, uv, tol)?;
152 let length = normal.magnitude();
153 if length <= tol.confusion() {
154 return Ok(None);
155 }
156 let normal = normal / length;
157 let on = boundary.faces_at(model, at, tol)?;
161 let mut net = 0_i64;
162 let mut first = true;
163 for other in &on {
164 let Some(gathered) = fused.a.faces.iter().position(|g| g.face.is_same(other)) else {
165 ogeom_bail!(NotDone, "a face of the solid was not gathered");
166 };
167 let theirs = &fused.a.faces[gathered];
168 let foot = ogeom_algo::project_on_surface(&theirs.surface, at, 16, tol)?;
169 let (u, v) = foot.parameters;
170 let along = outward_normal(theirs, Point2::new(u, v), tol)?.dot(normal);
171 if along > 0.0 {
172 net += 1;
173 if gathered < piece.face {
174 first = false;
175 }
176 } else {
177 net -= 1;
178 }
179 }
180 let Some((beyond, behind)) = sides(model, face, at, normal, &on, boundary, tol)? else {
181 return Ok(None);
182 };
183 if behind - beyond != net {
184 ogeom_bail!(
185 NotDone,
186 "a piece of a face reads windings {beyond} and {behind} on its two sides, \
187 which the {net} faces lying there do not step between"
188 );
189 }
190 if beyond >= 1 && behind <= 0 {
191 ogeom_bail!(
192 NotDone,
193 "a piece of a face bounds the union facing into it, which no face lying there \
194 is turned to"
195 );
196 }
197 Ok(Some(beyond == 0 && behind >= 1 && first))
198}
199
200fn sides(
206 model: &Model,
207 face: &GFace,
208 at: Point,
209 normal: Vector,
210 on: &[Shape],
211 boundary: &SolidBoundary,
212 tol: Tolerances,
213) -> OgeomResult<Option<(i64, i64)>> {
214 let shortest = face.tolerance.max(tol.confusion() * 1e3) * 20.0;
215 let mut step = (face.filter.diagonal() * 0.05).max(shortest);
216 loop {
217 ogeom_core::progress::checkpoint()?;
218 let (out_at, in_at) = (at + normal * step, at - normal * step);
219 if boundary.clear_from(model, at, out_at, on, tol)?
220 && boundary.clear_from(model, at, in_at, on, tol)?
221 && let (Some(beyond), Some(behind)) = (
222 boundary.winding(model, out_at, tol)?,
223 boundary.winding(model, in_at, tol)?,
224 )
225 {
226 return Ok(Some((beyond, behind)));
227 }
228 if step <= shortest {
229 return Ok(None);
230 }
231 step = (step * 0.1).max(shortest);
232 }
233}
234
235fn interior_point(
240 model: &Model,
241 face: &GFace,
242 piece: &FacePiece,
243 tol: Tolerances,
244) -> OgeomResult<Option<(Point, Point2)>> {
245 if !piece.outlines.is_empty() {
246 let foot = ogeom_algo::project_on_surface(&face.surface, piece.probe, 16, tol)?;
247 let (u, v) = foot.parameters;
248 return Ok(Some((piece.probe, Point2::new(u, v))));
249 }
250 let mesh = ogeom_mesh::triangulate_face(model, &face.face, Deflection::default(), tol)?;
251 if mesh.parameters.len() != mesh.positions.len() {
252 return Ok(None);
253 }
254 let area = |t: &[u32; 3]| {
255 let [a, b, c] = t.map(|i| mesh.positions[i as usize]);
256 (b - a).cross(c - a).magnitude()
257 };
258 let mut triangles: Vec<&[u32; 3]> = mesh.triangles.iter().collect();
259 triangles.sort_by(|x, y| area(y).total_cmp(&area(x)));
260 for triangle in triangles {
261 let corners = triangle.map(|i| mesh.parameters[i as usize]);
262 let uv = Point2::new(
263 (corners[0].0 + corners[1].0 + corners[2].0) / 3.0,
264 (corners[0].1 + corners[1].1 + corners[2].1) / 3.0,
265 );
266 let at = face.surface.point_at(uv.x, uv.y, tol)?;
267 if ogeom_algo::classify_on_face(model, &face.face, at, Deflection::default(), tol)?
268 == ogeom_algo::Containment::In
269 {
270 return Ok(Some((at, uv)));
271 }
272 }
273 Ok(None)
274}
275
276fn quiet_groups(fused: &GeneralFused) -> Vec<Vec<usize>> {
284 let quiet: Vec<usize> = fused
285 .pieces
286 .iter()
287 .enumerate()
288 .filter(|(_, piece)| piece.from_a && piece.outlines.is_empty())
289 .map(|(index, _)| index)
290 .collect();
291 let mut parent: Vec<usize> = (0..quiet.len()).collect();
292 let find = |parent: &mut Vec<usize>, mut i: usize| {
293 while parent[i] != i {
294 parent[i] = parent[parent[i]];
295 i = parent[i];
296 }
297 i
298 };
299 let mut uses: ogeom_core::FastMap<super::EdgeKey, usize> = ogeom_core::FastMap::default();
302 for face in &fused.a.faces {
303 for edge in &face.edges {
304 *uses.entry(edge.node).or_default() += 1;
305 }
306 }
307 let mut by_edge: ogeom_core::FastMap<super::EdgeKey, usize> = ogeom_core::FastMap::default();
308 for (slot, &index) in quiet.iter().enumerate() {
309 for edge in &fused.a.faces[fused.pieces[index].face].edges {
310 let key = edge.node;
311 if uses.get(&key) != Some(&2) {
312 continue;
313 }
314 if let Some(&other) = by_edge.get(&key) {
315 let (a, b) = (find(&mut parent, slot), find(&mut parent, other));
316 parent[a.max(b)] = a.min(b);
317 } else {
318 by_edge.insert(key, slot);
319 }
320 }
321 }
322 let mut groups: Vec<Vec<usize>> = Vec::new();
323 let mut root_group: ogeom_core::FastMap<usize, usize> = ogeom_core::FastMap::default();
324 for (slot, &index) in quiet.iter().enumerate() {
325 let root = find(&mut parent, slot);
326 let group = *root_group.entry(root).or_insert_with(|| {
327 groups.push(Vec::new());
328 groups.len() - 1
329 });
330 groups[group].push(index);
331 }
332 groups
333}