1use geop_core_geometry::nurb_curve::loop_signed_area;
18use geop_core_math::{
19 geop_error::{GeopError, GeopResult},
20 scalars::Scalar,
21};
22
23use crate::{
24 Sense,
25 build::{BodySpec, CoedgeOn, CoedgeSpec, EdgeSpec, FaceSpec},
26};
27
28thread_local! {
29 static SURFACES_COMPARED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
30}
31
32pub fn surfaces_compared() -> usize {
36 SURFACES_COMPARED.get()
37}
38
39#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct Merged {
43 pub faces: Vec<usize>,
45 pub edges: Vec<Option<usize>>,
48 pub vertices: Vec<Option<usize>>,
50}
51
52impl BodySpec {
53 pub fn merged(
67 &self,
68 edge_may_go: impl Fn(usize) -> bool,
69 vertex_may_go: impl Fn(usize) -> bool,
70 ) -> GeopResult<(BodySpec, Merged)> {
71 let mut work = Work::new(self);
72 while work.merge_one_pair(&edge_may_go)? {}
73 while work.join_one_pair(&vertex_may_go)? {}
74 work.compacted()
75 }
76}
77
78struct Work<'a> {
81 spec: &'a BodySpec,
82 faces: Vec<Option<FaceSpec>>,
84 face_into: Vec<usize>,
86 edges: Vec<Option<EdgeSpec>>,
88 edge_into: Vec<Option<usize>>,
91 apart: std::collections::HashSet<(usize, usize)>,
94}
95
96impl<'a> Work<'a> {
97 fn new(spec: &'a BodySpec) -> Self {
98 Self {
99 spec,
100 faces: spec.faces.iter().cloned().map(Some).collect(),
101 face_into: (0..spec.faces.len()).collect(),
102 edges: spec.edges.iter().cloned().map(Some).collect(),
103 edge_into: (0..spec.edges.len()).map(Some).collect(),
104 apart: Default::default(),
105 }
106 }
107
108 fn uses(&self) -> Vec<Vec<usize>> {
110 let mut uses = vec![Vec::new(); self.edges.len()];
111 for (f, face) in self.faces.iter().enumerate() {
112 let Some(face) = face else { continue };
113 for c in face.loops().into_iter().flatten() {
114 if let CoedgeOn::Edge(e, _) = c.on {
115 uses[e].push(f);
116 }
117 }
118 }
119 uses
120 }
121
122 fn ends(&self, on: CoedgeOn) -> (usize, usize) {
125 match on {
126 CoedgeOn::Edge(e, sense) => {
127 let edge = self.edges[e]
128 .as_ref()
129 .expect("a coedge runs along a live edge");
130 match sense {
131 Sense::Forward => (edge.start, edge.end),
132 Sense::Reversed => (edge.end, edge.start),
133 }
134 }
135 CoedgeOn::Vertex(v) => (v, v),
136 }
137 }
138
139 fn merge_one_pair(&mut self, edge_may_go: &impl Fn(usize) -> bool) -> GeopResult<bool> {
142 let uses = self.uses();
143 let pair = |f: usize, g: usize| (f.min(g), f.max(g));
144 let held: std::collections::HashSet<(usize, usize)> = (uses.iter().enumerate())
146 .filter_map(|(e, faces)| match faces.as_slice() {
147 [f, g] if !edge_may_go(e) => Some(pair(*f, *g)),
148 _ => None,
149 })
150 .collect();
151 for faces in &uses {
152 let [f, g] = faces.as_slice() else { continue };
153 if f == g {
154 continue;
155 }
156 let (keep, gone) = pair(*f, *g);
157 if held.contains(&(keep, gone)) || self.apart.contains(&(keep, gone)) {
158 continue;
159 }
160 let Some(face) = self.merged_face(keep, gone, &uses)? else {
161 self.apart.insert((keep, gone));
162 continue;
163 };
164 {
165 self.apart.retain(|&(a, b)| a != keep && b != keep);
166 self.faces[keep] = Some(face);
167 self.faces[gone] = None;
168 for into in self.face_into.iter_mut() {
169 if *into == gone {
170 *into = keep;
171 }
172 }
173 for (e, faces) in uses.iter().enumerate() {
174 if faces.len() == 2 && faces.iter().all(|&h| h == keep || h == gone) {
175 self.edges[e] = None;
176 self.edge_into[e] = None;
177 }
178 }
179 return Ok(true);
180 }
181 }
182 Ok(false)
183 }
184
185 fn merged_face(
188 &self,
189 keep: usize,
190 gone: usize,
191 uses: &[Vec<usize>],
192 ) -> GeopResult<Option<FaceSpec>> {
193 SURFACES_COMPARED.set(SURFACES_COMPARED.get() + 1);
194 let (a, b) = (
195 self.faces[keep].as_ref().expect("live"),
196 self.faces[gone].as_ref().expect("live"),
197 );
198 let (surface, coedges) = if let Some((surface, map)) = a.surface.carrying(&b.surface)? {
201 (surface, self.coedges(a, b, |c| map.apply_to(c))?)
202 } else if let Some((surface, map)) = b.surface.carrying(&a.surface)? {
203 (surface, self.coedges(b, a, |c| map.apply_to(c))?)
204 } else {
205 return Ok(None);
206 };
207 let inside = |c: &CoedgeSpec| match c.on {
209 CoedgeOn::Edge(e, _) => {
210 uses[e].len() == 2 && uses[e].iter().all(|&h| h == keep || h == gone)
211 }
212 CoedgeOn::Vertex(_) => false,
213 };
214 let coedges: Vec<CoedgeSpec> = coedges.into_iter().filter(|c| !inside(c)).collect();
215 let Some(loops) = self.loops(coedges)? else {
216 return Ok(None);
217 };
218 let mut outer = None;
219 let mut holes = Vec::new();
220 for lp in loops {
221 let area = loop_signed_area(lp.iter().map(|c| &c.pcurve))?;
222 if area.definitely_greater(Scalar::ZERO) && outer.is_none() {
223 outer = Some(lp);
224 } else if area.definitely_less(Scalar::ZERO) {
225 holes.push(lp);
226 } else {
227 return Ok(None);
228 }
229 }
230 Ok(outer.map(|outer| FaceSpec {
231 surface,
232 outer,
233 holes,
234 }))
235 }
236
237 fn coedges(
240 &self,
241 first: &FaceSpec,
242 second: &FaceSpec,
243 map: impl Fn(&crate::Curve2) -> GeopResult<crate::Curve2>,
244 ) -> GeopResult<Vec<CoedgeSpec>> {
245 let mut out: Vec<CoedgeSpec> = first.loops().into_iter().flatten().cloned().collect();
246 for c in second.loops().into_iter().flatten() {
247 out.push(CoedgeSpec {
248 on: c.on,
249 pcurve: map(&c.pcurve)?,
250 });
251 }
252 Ok(out)
253 }
254
255 fn loops(&self, coedges: Vec<CoedgeSpec>) -> GeopResult<Option<Vec<Vec<CoedgeSpec>>>> {
259 let mut leaving: std::collections::HashMap<usize, usize> = Default::default();
263 let mut rows: std::collections::HashMap<usize, usize> = Default::default();
264 for (i, c) in coedges.iter().enumerate() {
265 let at = if matches!(c.on, CoedgeOn::Vertex(_)) {
266 &mut rows
267 } else {
268 &mut leaving
269 };
270 if at.insert(self.ends(c.on).0, i).is_some() {
271 return Ok(None);
272 }
273 }
274 let mut used = vec![false; coedges.len()];
275 let mut loops = Vec::new();
276 for first in 0..coedges.len() {
277 if used[first] {
278 continue;
279 }
280 let mut lp = Vec::new();
281 let mut at = first;
282 loop {
283 if used[at] {
284 if at != first {
285 return Ok(None);
286 }
287 break;
288 }
289 used[at] = true;
290 lp.push(at);
291 let arrives = self.ends(coedges[at].on).1;
292 let along_row = matches!(coedges[at].on, CoedgeOn::Vertex(_));
293 let next = match (rows.get(&arrives), leaving.get(&arrives)) {
294 (Some(&row), _) if !along_row => row,
295 (_, Some(&next)) => next,
296 (Some(&row), None) => row,
297 (None, None) => return Ok(None),
298 };
299 let (end, start) = (coedges[at].pcurve.ends()?.1, coedges[next].pcurve.ends()?.0);
300 if !end.could_be_equal(&start) {
301 return Ok(None);
302 }
303 at = next;
304 }
305 loops.push(lp.into_iter().map(|i| coedges[i].clone()).collect());
306 }
307 Ok(Some(loops))
308 }
309
310 fn join_one_pair(&mut self, vertex_may_go: &impl Fn(usize) -> bool) -> GeopResult<bool> {
313 let uses = self.uses();
314 let n = self.spec.vertices.len();
315 let mut at_vertex: Vec<Vec<usize>> = vec![Vec::new(); n];
316 for (e, edge) in self.edges.iter().enumerate() {
317 let Some(edge) = edge else { continue };
318 at_vertex[edge.start].push(e);
319 at_vertex[edge.end].push(e);
320 }
321 let mut pole = vec![false; n];
322 for face in self.faces.iter().flatten() {
323 for c in face.loops().into_iter().flatten() {
324 if let CoedgeOn::Vertex(v) = c.on {
325 pole[v] = true;
326 }
327 }
328 }
329 for v in 0..n {
330 let [e1, e2] = at_vertex[v].as_slice() else {
331 continue;
332 };
333 let (e1, e2) = ((*e1).min(*e2), (*e1).max(*e2));
334 if e1 == e2 || pole[v] || !vertex_may_go(v) {
335 continue;
336 }
337 let (one, two) = (
338 self.edges[e1].as_ref().unwrap(),
339 self.edges[e2].as_ref().unwrap(),
340 );
341 let far = |edge: &EdgeSpec| {
342 if edge.start == v {
343 edge.end
344 } else {
345 edge.start
346 }
347 };
348 if far(one) == far(two) || one.start == one.end || two.start == two.end {
349 continue;
350 }
351 let mut faces = (uses[e1].clone(), uses[e2].clone());
352 faces.0.sort();
353 faces.1.sort();
354 if faces.0 != faces.1 || faces.0.len() != 2 || faces.0[0] == faces.0[1] {
355 continue;
356 }
357 let tangent_at_v = |edge: &EdgeSpec| {
360 let (t0, t1) = edge.curve.domain();
361 edge.curve.tangent(if edge.start == v { t0 } else { t1 })
362 };
363 if !tangent_at_v(one)?.could_be_parallel(&tangent_at_v(two)?) {
364 continue;
365 }
366 if self.join(v, e1, e2)? {
367 return Ok(true);
368 }
369 }
370 Ok(false)
371 }
372
373 fn join(&mut self, v: usize, e1: usize, e2: usize) -> GeopResult<bool> {
377 let (one, two) = (
378 self.edges[e1].clone().unwrap(),
379 self.edges[e2].clone().unwrap(),
380 );
381 let toward_v = |edge: &EdgeSpec| {
383 if edge.end == v {
384 edge.curve.clone()
385 } else {
386 edge.curve.reverse()
387 }
388 };
389 let (curve, start, end) = if one.end == v {
390 let from_v = toward_v(&two).reverse();
391 (
392 one.curve.joined(&from_v),
393 one.start,
394 if two.start == v { two.end } else { two.start },
395 )
396 } else {
397 let into_v = toward_v(&two);
398 (
399 into_v.joined(&one.curve),
400 if two.end == v { two.start } else { two.end },
401 one.end,
402 )
403 };
404 let Ok(curve) = curve else {
405 return Ok(false);
406 };
407 let mut faces = self.faces.clone();
409 for face in faces.iter_mut().flatten() {
410 let mut loops: Vec<Vec<CoedgeSpec>> = face.loops().into_iter().cloned().collect();
411 for lp in &mut loops {
412 let n = lp.len();
413 let on = |c: &CoedgeSpec, e: usize| matches!(c.on, CoedgeOn::Edge(x, _) if x == e);
414 let Some(k) = (0..n).find(|&k| {
415 let next = &lp[(k + 1) % n];
416 (on(&lp[k], e1) && on(next, e2)) || (on(&lp[k], e2) && on(next, e1))
417 }) else {
418 continue;
419 };
420 let (first, second) = (lp[k].clone(), lp[(k + 1) % n].clone());
421 let Ok(pcurve) = first.pcurve.joined(&second.pcurve) else {
422 return Ok(false);
423 };
424 let from = self.ends(first.on).0;
428 let sense = if from == start {
429 Sense::Forward
430 } else if from == end {
431 Sense::Reversed
432 } else {
433 return Err(GeopError::new(format!(
434 "BodySpec::merged: a loop runs edges {e1} and {e2} from vertex {from}, which neither ends"
435 )));
436 };
437 let joined = CoedgeSpec {
438 on: CoedgeOn::Edge(e1, sense),
439 pcurve,
440 };
441 if k + 1 < n {
442 lp.splice(k..k + 2, [joined]);
443 } else {
444 lp.remove(k);
446 lp[0] = joined;
447 }
448 }
449 let outer = loops.remove(0);
450 *face = FaceSpec {
451 surface: face.surface.clone(),
452 outer,
453 holes: loops,
454 };
455 }
456 self.faces = faces;
457 self.edges[e1] = Some(EdgeSpec { curve, start, end });
458 self.edges[e2] = None;
459 for into in self.edge_into.iter_mut() {
460 if *into == Some(e2) {
461 *into = Some(e1);
462 }
463 }
464 Ok(true)
465 }
466
467 fn compacted(self) -> GeopResult<(BodySpec, Merged)> {
470 let spec = self.spec;
471 let mut face_index = vec![None; spec.faces.len()];
472 let mut faces = Vec::new();
473 for (f, face) in self.faces.iter().enumerate() {
474 if let Some(face) = face {
475 face_index[f] = Some(faces.len());
476 faces.push(face.clone());
477 }
478 }
479 let mut vertex_index = vec![None; spec.vertices.len()];
480 let mut vertices = Vec::new();
481 let mut vertex = |v: usize, vertices: &mut Vec<_>| {
482 *vertex_index[v].get_or_insert_with(|| {
483 vertices.push(spec.vertices[v]);
484 vertices.len() - 1
485 })
486 };
487 let mut edge_index = vec![None; spec.edges.len()];
488 let mut edges = Vec::new();
489 for (e, edge) in self.edges.iter().enumerate() {
490 if let Some(edge) = edge {
491 edge_index[e] = Some(edges.len());
492 edges.push(EdgeSpec {
493 curve: edge.curve.clone(),
494 start: vertex(edge.start, &mut vertices),
495 end: vertex(edge.end, &mut vertices),
496 });
497 }
498 }
499 for face in &mut faces {
500 let mut renumber = |lp: &mut Vec<CoedgeSpec>, vertices: &mut Vec<_>| {
501 for c in lp {
502 c.on = match c.on {
503 CoedgeOn::Edge(e, sense) => {
504 CoedgeOn::Edge(edge_index[e].expect("a live edge"), sense)
505 }
506 CoedgeOn::Vertex(v) => CoedgeOn::Vertex(vertex(v, vertices)),
507 };
508 }
509 };
510 renumber(&mut face.outer, &mut vertices);
511 for hole in &mut face.holes {
512 renumber(hole, &mut vertices);
513 }
514 }
515 let shells = (spec.shells.iter())
516 .map(|members| {
517 let mut kept: Vec<usize> = members.iter().filter_map(|&f| face_index[f]).collect();
518 kept.dedup();
519 kept
520 })
521 .collect();
522 let out = BodySpec {
523 vertices,
524 edges,
525 faces,
526 shells,
527 solid: spec.solid,
528 };
529 out.check()?;
530 let merged = Merged {
531 faces: (self.face_into.iter())
532 .map(|&f| face_index[f].expect("a face is part of a live one"))
533 .collect(),
534 edges: (self.edge_into.iter())
535 .map(|e| e.and_then(|e| edge_index[e]))
536 .collect(),
537 vertices: vertex_index,
538 };
539 Ok((out, merged))
540 }
541}
542
543#[cfg(test)]
544mod tests {
545
546 use crate::{
547 Model,
548 test_fixtures::test_cube_solid,
549 validation::{ValidationParameters, validate},
550 };
551
552 #[test]
554 fn a_cube_stays_a_cube() {
555 let mut model = Model::new();
556 let solid = test_cube_solid(&mut model);
557 let faces = model.solid_faces(solid).unwrap();
558 let (spec, _) = model.body_spec(&faces, true).unwrap();
559 let (merged, map) = spec.merged(|_| true, |_| true).unwrap();
560 assert_eq!(merged.faces.len(), 6);
561 assert_eq!(merged.edges.len(), 12);
562 assert_eq!(merged.vertices.len(), 8);
563 assert_eq!(map.faces, (0..6).collect::<Vec<_>>());
564 let mut model = Model::new();
565 model.build_body(merged).unwrap();
566 validate(&ValidationParameters::default(), &model).unwrap();
567 }
568}