1use std::cmp::Ordering;
13
14use fast_mesh_store::{Mesh, Transaction, VertexId};
15
16use crate::topology::{BoundaryLoop, EdgeMap, boundary_loops, cross, dot, point, sub};
17use crate::{LoopId, OpsError, Result, push, reserve};
18
19pub const BRIDGE_DEFAULT_MAX_EDGES: usize = 128;
23
24#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct BridgeOptions {
27 pub loops: (LoopId, LoopId),
30 pub max_edges: Option<usize>,
32}
33
34impl BridgeOptions {
35 pub fn builder(a: LoopId, b: LoopId) -> BridgeOptionsBuilder {
37 BridgeOptionsBuilder {
38 loops: (a, b),
39 max_edges: None,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy)]
46pub struct BridgeOptionsBuilder {
47 loops: (LoopId, LoopId),
48 max_edges: Option<usize>,
49}
50
51impl BridgeOptionsBuilder {
52 pub fn max_edges(mut self, max: usize) -> Self {
54 self.max_edges = Some(max);
55 self
56 }
57
58 pub fn build(self) -> BridgeOptions {
60 BridgeOptions {
61 loops: self.loops,
62 max_edges: self.max_edges,
63 }
64 }
65}
66
67#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
69pub struct BridgeReport {
70 pub triangles_added: usize,
72}
73
74pub fn bridge_loops(tx: &mut Transaction, options: &BridgeOptions) -> Result<BridgeReport> {
82 let (ida, idb) = options.loops;
83 if ida == idb {
84 return Err(OpsError::InvalidInput(
85 "bridge loops must be two distinct loops",
86 ));
87 }
88 let mesh = tx.mesh();
89 let loops = boundary_loops(mesh)?;
90 let la = resolve(&loops, &ida)?;
91 let lb = resolve(&loops, &idb)?;
92 let (first, second) = if loop_key(&ida) <= loop_key(&idb) {
93 (la, lb)
94 } else {
95 (lb, la)
96 };
97 for l in [first, second] {
98 if !l.closed || !l.clean || l.vertices.len() < 3 {
99 return Err(OpsError::InvalidInput(
100 "bridge loops must be closed and clean",
101 ));
102 }
103 }
104 let bound = options.max_edges.unwrap_or(BRIDGE_DEFAULT_MAX_EDGES);
105 if first.vertices.len() > bound || second.vertices.len() > bound {
106 return Err(OpsError::InvalidInput("bridge loop exceeds max_edges"));
107 }
108 for &v in &first.vertices {
109 if second.vertices.contains(&v) {
110 return Err(OpsError::InvalidInput(
111 "bridge loops must not share vertices",
112 ));
113 }
114 }
115 for l in [first, second] {
116 for &v in &l.vertices {
117 if !point(mesh, v).iter().all(|c| c.is_finite()) {
118 return Err(OpsError::InvalidInput(
119 "bridge loop positions must be finite",
120 ));
121 }
122 }
123 }
124 let na = newell(mesh, &first.vertices);
125 let nb = newell(mesh, &second.vertices);
126 if !matches!(dot(na, nb).partial_cmp(&0.0), Some(Ordering::Less)) {
127 return Err(OpsError::InvalidInput(
128 "bridge loops must wind in opposite directions",
129 ));
130 }
131 let strip = best_strip(mesh, &first.vertices, &second.vertices)?;
132 for t in &strip {
133 let area2 = tri_area2(&point(mesh, t[0]), &point(mesh, t[1]), &point(mesh, t[2]));
134 if area2 == 0.0 {
135 return Err(OpsError::InvalidInput("bridge triangulation is degenerate"));
136 }
137 }
138 let edges = EdgeMap::build(mesh)?;
139 let mut chords = chords_of(&strip, &first.vertices)?;
140 chords.sort_unstable();
141 chords.dedup();
142 for &(a, b) in &chords {
143 if edges.kind(a, b).is_some() {
144 return Err(OpsError::InvalidInput("bridge diagonal already exists"));
145 }
146 }
147 let report = BridgeReport {
148 triangles_added: strip.len(),
149 };
150 tx.add_triangles(&strip)?;
151 Ok(report)
152}
153
154fn loop_key(id: &LoopId) -> (u32, u32, u64) {
155 (id.min_vertex, id.len, id.hash)
156}
157
158fn resolve<'a>(loops: &'a [BoundaryLoop], id: &LoopId) -> Result<&'a BoundaryLoop> {
160 let at = loops.partition_point(|l| l.id.min_vertex < id.min_vertex);
161 loops
162 .get(at)
163 .filter(|l| l.id == *id)
164 .ok_or(OpsError::StaleLoops)
165}
166
167fn newell(mesh: &Mesh, verts: &[VertexId]) -> [f64; 3] {
169 let mut n = [0.0; 3];
170 for k in 0..verts.len() {
171 let c = cross(
172 point(mesh, verts[k]),
173 point(mesh, verts[(k + 1) % verts.len()]),
174 );
175 n[0] += c[0];
176 n[1] += c[1];
177 n[2] += c[2];
178 }
179 n
180}
181
182fn tri_area2(a: &[f64; 3], b: &[f64; 3], c: &[f64; 3]) -> f64 {
184 let n = cross(sub(*b, *a), sub(*c, *a));
185 dot(n, n).sqrt()
186}
187
188fn best_strip(mesh: &Mesh, first: &[VertexId], second: &[VertexId]) -> Result<Vec<[VertexId; 3]>> {
192 let n = first.len();
193 let m = second.len();
194 let mut xa: Vec<VertexId> = Vec::new();
197 reserve(&mut xa, n + 1)?;
198 for i in 0..=n {
199 push(&mut xa, first[(n - i) % n])?;
200 }
201 let mut px: Vec<[f64; 3]> = Vec::new();
202 reserve(&mut px, n + 1)?;
203 for &v in &xa {
204 push(&mut px, point(mesh, v))?;
205 }
206 let mut py: Vec<[f64; 3]> = Vec::new();
207 reserve(&mut py, 2 * m + 1)?;
208 for k in 0..=2 * m {
209 push(&mut py, point(mesh, second[k % m]))?;
210 }
211 let cells = (n + 1) * (m + 1);
212 let mut cost: Vec<f64> = Vec::new();
213 reserve(&mut cost, cells)?;
214 cost.extend(std::iter::repeat_n(f64::INFINITY, cells));
215 let mut back: Vec<u8> = Vec::new();
216 reserve(&mut back, cells)?;
217 back.extend(std::iter::repeat_n(0, cells));
218 let mut best = f64::INFINITY;
219 let mut best_s = 0;
220 for s in 0..m {
221 let total = dp_fill(&px, &py[s..s + m + 1], &mut cost, &mut back);
222 if total < best {
223 best = total;
224 best_s = s;
225 }
226 }
227 dp_fill(&px, &py[best_s..best_s + m + 1], &mut cost, &mut back);
228 let mut strip: Vec<[VertexId; 3]> = Vec::new();
231 reserve(&mut strip, n + m)?;
232 let (mut i, mut k) = (n, m);
233 while i > 0 || k > 0 {
234 let step_a = if k == 0 {
235 true
236 } else if i == 0 {
237 false
238 } else {
239 back[i * (m + 1) + k] == 0
240 };
241 if step_a {
242 push(&mut strip, [xa[i - 1], xa[i], second[(best_s + k) % m]])?;
243 i -= 1;
244 } else {
245 push(
246 &mut strip,
247 [
248 xa[i],
249 second[(best_s + k) % m],
250 second[(best_s + k - 1) % m],
251 ],
252 )?;
253 k -= 1;
254 }
255 }
256 strip.reverse();
257 Ok(strip)
258}
259
260fn dp_fill(px: &[[f64; 3]], yb: &[[f64; 3]], cost: &mut [f64], back: &mut [u8]) -> f64 {
264 let n = px.len() - 1;
265 let m = yb.len() - 1;
266 for c in cost.iter_mut() {
267 *c = f64::INFINITY;
268 }
269 cost[0] = 0.0;
270 for i in 0..=n {
271 for k in 0..=m {
272 if i == 0 && k == 0 {
273 continue;
274 }
275 let mut best = f64::INFINITY;
276 let mut dir = 0u8;
277 if i > 0 {
278 let c = cost[(i - 1) * (m + 1) + k] + tri_area2(&px[i - 1], &px[i], &yb[k]);
279 if c <= best {
280 best = c;
281 dir = 0;
282 }
283 }
284 if k > 0 {
285 let c = cost[i * (m + 1) + k - 1] + tri_area2(&px[i], &yb[k], &yb[k - 1]);
286 if c < best {
287 best = c;
288 dir = 1;
289 }
290 }
291 cost[i * (m + 1) + k] = best;
292 back[i * (m + 1) + k] = dir;
293 }
294 }
295 cost[n * (m + 1) + m]
296}
297
298fn chords_of(strip: &[[VertexId; 3]], first: &[VertexId]) -> Result<Vec<(VertexId, VertexId)>> {
302 let mut a_sorted: Vec<VertexId> = Vec::new();
303 reserve(&mut a_sorted, first.len())?;
304 a_sorted.extend_from_slice(first);
305 a_sorted.sort_unstable();
306 let mut chords: Vec<(VertexId, VertexId)> = Vec::new();
307 reserve(&mut chords, 2 * strip.len())?;
308 for t in strip {
309 for (p, q) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
310 let pa = a_sorted.binary_search(&p).is_ok();
311 let qa = a_sorted.binary_search(&q).is_ok();
312 if pa != qa {
313 push(&mut chords, (p.min(q), p.max(q)))?;
314 }
315 }
316 }
317 Ok(chords)
318}
319
320#[cfg(test)]
321mod tests {
322 use fast_mesh_store::{Mesh, Transaction};
323
324 use super::*;
325 use crate::test_util::{edit, mesh_of, parts, sweep};
326 use crate::topology::EdgeMap;
327 use crate::{OpsError, analyze, boundary_loops};
328
329 fn two_quads() -> Mesh {
332 mesh_of(
333 &[
334 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0,
336 ],
337 &[[0, 2, 1], [0, 3, 2], [4, 5, 6], [4, 6, 7]],
338 )
339 }
340
341 fn tri_square() -> Mesh {
343 mesh_of(
344 &[
345 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0,
347 ],
348 &[[0, 2, 1], [3, 4, 5], [3, 5, 6]],
349 )
350 }
351
352 fn loop_of(mesh: &Mesh, min_vertex: u32) -> crate::LoopId {
353 boundary_loops(mesh)
354 .unwrap()
355 .iter()
356 .find(|l| l.id.min_vertex == min_vertex)
357 .unwrap()
358 .id
359 }
360
361 fn assert_outward(mesh: &Mesh, before: usize) {
363 let (pos, tris) = parts(mesh);
364 let p = |v: u32| -> [f64; 3] {
365 let i = 3 * v as usize;
366 [
367 f64::from(pos[i]),
368 f64::from(pos[i + 1]),
369 f64::from(pos[i + 2]),
370 ]
371 };
372 let mut ca = [0.0; 3];
374 let mut cb = [0.0; 3];
375 for v in 0..4u32 {
376 let q = p(v);
377 for k in 0..3 {
378 ca[k] += q[k] / 4.0;
379 }
380 }
381 for v in 4..8u32 {
382 let q = p(v);
383 for k in 0..3 {
384 cb[k] += q[k] / 4.0;
385 }
386 }
387 assert!(tris.len() > before);
388 for t in &tris[before..] {
389 let (a, b, c) = (p(t[0]), p(t[1]), p(t[2]));
390 let n = [
391 (b[1] - a[1]) * (c[2] - a[2]) - (b[2] - a[2]) * (c[1] - a[1]),
392 (b[2] - a[2]) * (c[0] - a[0]) - (b[0] - a[0]) * (c[2] - a[2]),
393 (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]),
394 ];
395 let m = [
396 (a[0] + b[0] + c[0]) / 3.0 - (ca[0] + cb[0]) / 2.0,
397 (a[1] + b[1] + c[1]) / 3.0 - (ca[1] + cb[1]) / 2.0,
398 0.0,
399 ];
400 let d = n[0] * m[0] + n[1] * m[1] + n[2] * m[2];
401 assert!(d > 0.0, "inward or degenerate: {t:?} d={d}");
402 }
403 }
404
405 #[test]
406 fn bridge_zips_two_square_loops() {
407 let mut mesh = two_quads();
408 let before = parts(&mesh).1.len();
409 let a = loop_of(&mesh, 0);
410 let b = loop_of(&mesh, 4);
411 let options = BridgeOptions::builder(a, b).build();
412 let (report, _) = edit(&mut mesh, |tx| bridge_loops(tx, &options));
413 assert_eq!(report.triangles_added, 8);
414 assert_outward(&mesh, before);
415 let edges = EdgeMap::build(&mesh).unwrap();
417 let (_, tris) = parts(&mesh);
418 for t in &tris[before..] {
419 for (x, y) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
420 assert_eq!(edges.triangles(x, y).len(), 2, "edge ({x}, {y})");
421 }
422 }
423 let conflicts = analyze(&mesh, &Default::default())
424 .unwrap()
425 .orientation_conflicts;
426 assert_eq!(conflicts, 0);
427 }
428
429 #[test]
430 fn bridge_handles_mismatched_loop_sizes() {
431 let mut mesh = tri_square();
432 let before = parts(&mesh).1.len();
433 let a = loop_of(&mesh, 0);
434 let b = loop_of(&mesh, 3);
435 let options = BridgeOptions::builder(a, b).build();
436 let (report, _) = edit(&mut mesh, |tx| bridge_loops(tx, &options));
437 assert_eq!(report.triangles_added, 7);
438 let edges = EdgeMap::build(&mesh).unwrap();
439 let (_, tris) = parts(&mesh);
440 for t in &tris[before..] {
441 for (x, y) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
442 assert_eq!(edges.triangles(x, y).len(), 2, "edge ({x}, {y})");
443 }
444 }
445 let conflicts = analyze(&mesh, &Default::default())
446 .unwrap()
447 .orientation_conflicts;
448 assert_eq!(conflicts, 0);
449 }
450
451 #[test]
452 fn bridge_rejects_same_winding() {
453 let mut mesh = mesh_of(
456 &[
457 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0,
459 ],
460 &[[0, 1, 2], [0, 2, 3], [4, 5, 6], [4, 6, 7]],
461 );
462 let before = parts(&mesh);
463 let a = loop_of(&mesh, 0);
464 let b = loop_of(&mesh, 4);
465 let options = BridgeOptions::builder(a, b).build();
466 let mut tx = mesh.begin_edit();
467 assert!(matches!(
468 bridge_loops(&mut tx, &options),
469 Err(OpsError::InvalidInput(_))
470 ));
471 drop(tx);
472 assert_eq!(parts(&mesh), before);
473 }
474
475 #[test]
476 fn bridge_rejects_bad_selection() {
477 let mut mesh = two_quads();
478 let before = parts(&mesh);
479 let a = loop_of(&mesh, 0);
480 let b = loop_of(&mesh, 4);
481 let options = BridgeOptions::builder(a, a).build();
483 let mut tx = mesh.begin_edit();
484 assert!(matches!(
485 bridge_loops(&mut tx, &options),
486 Err(OpsError::InvalidInput(_))
487 ));
488 drop(tx);
489 let fake = crate::LoopId {
491 min_vertex: 99,
492 len: 4,
493 hash: 7,
494 };
495 let options = BridgeOptions::builder(a, fake).build();
496 let mut tx = mesh.begin_edit();
497 assert_eq!(bridge_loops(&mut tx, &options), Err(OpsError::StaleLoops));
498 drop(tx);
499 let options = BridgeOptions::builder(a, b).max_edges(3).build();
501 let mut tx = mesh.begin_edit();
502 assert!(matches!(
503 bridge_loops(&mut tx, &options),
504 Err(OpsError::InvalidInput(_))
505 ));
506 drop(tx);
507 assert_eq!(parts(&mesh), before);
508 }
509
510 #[test]
511 fn bridge_sweep_keeps_atomicity() {
512 let mesh = two_quads();
513 let a = loop_of(&mesh, 0);
514 let b = loop_of(&mesh, 4);
515 let options = BridgeOptions::builder(a, b).build();
516 let n = sweep(mesh, |tx: &mut Transaction| {
517 bridge_loops(tx, &options)?;
518 Ok(())
519 });
520 assert!(n > 0);
521 }
522}