Skip to main content

fast_mesh_ops/
remesh.rs

1//! Region remeshing: matching a brush region's density to a target edge
2//! length.
3//!
4//! [`remesh`] runs rounds of midpoint splits, midpoint collapses, and
5//! Delaunay-style flips over the edges under a [`Region`], until a round
6//! changes nothing or `max_rounds` runs out, and commits through one
7//! [`Transaction`](fast_mesh_store::Transaction). New vertices sit at edge
8//! midpoints; existing positions are never modified.
9
10use std::cmp::Ordering;
11
12use fast_mesh_store::{Mesh, Transaction, TriangleId, VertexId};
13
14use crate::{OpsError, Result, push, reserve};
15
16/// Edges longer than this multiple of the target length are split.
17pub const SPLIT_FACTOR: f64 = 4.0 / 3.0;
18/// Edges shorter than this multiple of the target length collapse.
19pub const COLLAPSE_FACTOR: f64 = 4.0 / 5.0;
20/// Default cap on remesh rounds.
21pub const REMESH_MAX_ROUNDS: u32 = 10;
22
23/// A spherical region of influence. An edge takes part when its midpoint
24/// lies in the strict interior; the boundary itself is inert.
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct Region {
27    /// Center of the region in mesh units.
28    pub center: [f32; 3],
29    /// Radius of influence in mesh units. Must be positive and finite.
30    pub radius: f32,
31}
32
33impl Default for Region {
34    fn default() -> Self {
35        Region {
36            center: [0.0; 3],
37            radius: 1.0,
38        }
39    }
40}
41
42/// Options for [`remesh`].
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct RemeshOptions {
45    /// The region whose edges are remeshed.
46    pub region: Region,
47    /// Target edge length in mesh units. `None` resolves to the mesh-wide
48    /// mean live-edge length; non-finite or non-positive values fall back
49    /// to the mesh mean as well.
50    pub target_length: Option<f32>,
51    /// Maximum number of split/collapse/flip rounds.
52    pub max_rounds: u32,
53    /// Split long edges only: no collapses, no flips. The triangle count
54    /// only grows, so a live sculpt pass never pops the surface down.
55    pub split_only: bool,
56}
57
58impl Default for RemeshOptions {
59    fn default() -> Self {
60        RemeshOptions {
61            region: Region::default(),
62            target_length: None,
63            max_rounds: REMESH_MAX_ROUNDS,
64            split_only: false,
65        }
66    }
67}
68
69impl RemeshOptions {
70    /// A builder for [`RemeshOptions`]: the default region until set.
71    pub fn builder() -> RemeshOptionsBuilder {
72        RemeshOptionsBuilder::default()
73    }
74}
75
76/// Builder for [`RemeshOptions`].
77#[derive(Debug, Clone, Default)]
78pub struct RemeshOptionsBuilder {
79    options: RemeshOptions,
80}
81
82impl RemeshOptionsBuilder {
83    /// Center of the region in mesh units.
84    pub fn center(mut self, center: [f32; 3]) -> Self {
85        self.options.region.center = center;
86        self
87    }
88
89    /// Radius of influence in mesh units.
90    pub fn radius(mut self, radius: f32) -> Self {
91        self.options.region.radius = radius;
92        self
93    }
94
95    /// Target edge length in mesh units.
96    pub fn target_length(mut self, length: f32) -> Self {
97        self.options.target_length = Some(length);
98        self
99    }
100
101    /// Maximum number of rounds.
102    pub fn max_rounds(mut self, rounds: u32) -> Self {
103        self.options.max_rounds = rounds;
104        self
105    }
106
107    /// Split long edges only: no collapses, no flips.
108    pub fn split_only(mut self, split_only: bool) -> Self {
109        self.options.split_only = split_only;
110        self
111    }
112
113    /// The options as set. Region validity is checked by [`remesh`].
114    pub fn build(self) -> RemeshOptions {
115        self.options
116    }
117}
118
119/// Outcome of [`remesh`].
120#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
121pub struct RemeshReport {
122    /// Edge splits performed.
123    pub splits: usize,
124    /// Edge collapses performed.
125    pub collapses: usize,
126    /// Edge flips performed.
127    pub flips: usize,
128    /// Rounds that changed the mesh.
129    pub rounds: usize,
130}
131
132/// Remeshes the edges inside `options.region` toward the target length.
133///
134/// Each round splits the edges whose midpoint lies in the region and whose
135/// length exceeds the target by [`SPLIT_FACTOR`], collapses interior edges
136/// shorter than the target by [`COLLAPSE_FACTOR`], and flips interior edges
137/// whose quad gains a larger minimum angle. Splitting an edge inserts one
138/// vertex at its midpoint, shared by the triangles on either side, so
139/// neighbors stay stitched; collapsing an edge merges its endpoints into one
140/// midpoint vertex. Boundary edges never collapse, and any pass that would
141/// fold the surface is skipped. The whole plan is computed on working copies
142/// first and committed through `tx` at the end, which keeps the call atomic
143/// under sweep: either every round lands or nothing does.
144pub fn remesh(tx: &mut Transaction, options: &RemeshOptions) -> Result<RemeshReport> {
145    let region = &options.region;
146    if !region.radius.is_finite() || region.radius <= 0.0 {
147        return Err(OpsError::InvalidInput(
148            "remesh region radius must be positive and finite",
149        ));
150    }
151    if region.center.iter().any(|c| !c.is_finite()) {
152        return Err(OpsError::InvalidInput(
153            "remesh region center must be finite",
154        ));
155    }
156    let mesh = tx.mesh();
157    if !has_member(mesh, region) {
158        return Ok(RemeshReport::default());
159    }
160    let sigma = match options.target_length {
161        Some(t) if t.is_finite() && t > 0.0 => f64::from(t),
162        _ => match global_mean_edge_length(mesh)? {
163            Some(mean) => mean,
164            None => return Ok(RemeshReport::default()),
165        },
166    };
167    let base = mesh.positions();
168    let mut positions: Vec<[f32; 3]> = Vec::new();
169    reserve(&mut positions, base.len())?;
170    positions.extend_from_slice(base);
171    let slots = positions.len();
172    let mut tris: Vec<WorkTri> = Vec::new();
173    reserve(&mut tris, mesh.triangle_slots())?;
174    for t in 0..mesh.triangle_slots() as u32 {
175        if let Some(corners) = mesh.triangle(t) {
176            push(
177                &mut tris,
178                WorkTri {
179                    id: Some(t),
180                    corners,
181                    alive: true,
182                },
183            )?;
184        }
185    }
186    let mut vlive: Vec<bool> = Vec::new();
187    reserve(&mut vlive, slots)?;
188    vlive.extend(std::iter::repeat_n(true, slots));
189    let mut added: Vec<NewVert> = Vec::new();
190    let mut report = RemeshReport::default();
191    for _ in 0..options.max_rounds {
192        let fresh = positions.len();
193        let splits = split_round(
194            &mut positions,
195            &mut vlive,
196            &mut tris,
197            &mut added,
198            region,
199            sigma,
200        )?;
201        let (collapses, flips) = if options.split_only {
202            (0, 0)
203        } else {
204            let collapses = collapse_round(
205                &mut positions,
206                &mut vlive,
207                &mut tris,
208                &mut added,
209                region,
210                sigma,
211                fresh,
212            )?;
213            let flips = flip_round(&positions, &vlive, &mut tris, region, sigma)?;
214            (collapses, flips)
215        };
216        if splits + collapses + flips == 0 {
217            break;
218        }
219        report.splits += splits;
220        report.collapses += collapses;
221        report.flips += flips;
222        report.rounds += 1;
223    }
224    if report.splits + report.collapses + report.flips == 0 {
225        return Ok(report);
226    }
227    commit(tx, slots, &vlive, &tris, &added)?;
228    Ok(report)
229}
230
231/// A working triangle: `id` is `None` for triangles created by a split.
232struct WorkTri {
233    id: Option<TriangleId>,
234    corners: [u32; 3],
235    alive: bool,
236}
237
238/// A midpoint vertex waiting for commit. `parents` weights the new vertex
239/// over live base vertices so channels interpolate; positions are exact.
240pub(crate) struct NewVert {
241    pub(crate) pos: [f32; 3],
242    pub(crate) parents: Vec<(VertexId, f32)>,
243}
244
245fn in_region(p: &[f32; 3], region: &Region) -> bool {
246    let dx = f64::from(p[0]) - f64::from(region.center[0]);
247    let dy = f64::from(p[1]) - f64::from(region.center[1]);
248    let dz = f64::from(p[2]) - f64::from(region.center[2]);
249    let t = dx.hypot(dy).hypot(dz) / f64::from(region.radius);
250    t.is_finite() && t < 1.0
251}
252
253/// Whether any live edge midpoint lies in the region. The passes test
254/// midpoints, not vertices, so a vertex scan would wrongly early-out a
255/// region crossed by a long edge but holding no vertices.
256fn has_member(mesh: &Mesh, region: &Region) -> bool {
257    let positions = mesh.positions();
258    for t in 0..mesh.triangle_slots() as u32 {
259        let corners = match mesh.triangle(t) {
260            Some(c) => c,
261            None => continue,
262        };
263        if degenerate(&corners) {
264            continue;
265        }
266        for k in 0..3 {
267            let mid = midpoint(
268                &positions[corners[k] as usize],
269                &positions[corners[(k + 1) % 3] as usize],
270            );
271            if in_region(&mid, region) {
272                return true;
273            }
274        }
275    }
276    false
277}
278
279fn dist(a: &[f32; 3], b: &[f32; 3]) -> f64 {
280    let dx = f64::from(a[0]) - f64::from(b[0]);
281    let dy = f64::from(a[1]) - f64::from(b[1]);
282    let dz = f64::from(a[2]) - f64::from(b[2]);
283    dx.hypot(dy).hypot(dz)
284}
285
286pub(crate) fn midpoint(a: &[f32; 3], b: &[f32; 3]) -> [f32; 3] {
287    [
288        (a[0] + b[0]) / 2.0,
289        (a[1] + b[1]) / 2.0,
290        (a[2] + b[2]) / 2.0,
291    ]
292}
293
294fn global_mean_edge_length(mesh: &Mesh) -> Result<Option<f64>> {
295    let mut edges: Vec<(u32, u32)> = Vec::new();
296    reserve(&mut edges, 3 * mesh.triangle_slots())?;
297    for t in 0..mesh.triangle_slots() as u32 {
298        let corners = match mesh.triangle(t) {
299            Some(c) => c,
300            None => continue,
301        };
302        if degenerate(&corners) {
303            continue;
304        }
305        for k in 0..3 {
306            let (a, b) = (corners[k], corners[(k + 1) % 3]);
307            push(&mut edges, (a.min(b), a.max(b)))?;
308        }
309    }
310    edges.sort_unstable();
311    edges.dedup();
312    if edges.is_empty() {
313        return Ok(None);
314    }
315    let positions = mesh.positions();
316    let mut sum = 0.0;
317    for &(a, b) in &edges {
318        sum += dist(&positions[a as usize], &positions[b as usize]);
319    }
320    let mean = sum / edges.len() as f64;
321    Ok((mean.is_finite() && mean > 0.0).then_some(mean))
322}
323
324/// Splits every overlong in-region edge once. Returns the split count.
325/// Each unique edge is visited once per round; an edge is skipped when a
326/// split made earlier in the same round already consumed it.
327fn split_round(
328    positions: &mut Vec<[f32; 3]>,
329    vlive: &mut Vec<bool>,
330    tris: &mut Vec<WorkTri>,
331    added: &mut Vec<NewVert>,
332    region: &Region,
333    sigma: f64,
334) -> Result<usize> {
335    // `positions` and `added` grow in lockstep, one entry per split, so
336    // their length gap is the base slot count for the whole round.
337    let slots = positions.len() - added.len();
338    let pairs = snapshot_pairs(tris, vlive)?;
339    let mut splits = 0;
340    let mut start = 0;
341    while start < pairs.len() {
342        let mut end = start + 1;
343        while end < pairs.len() && pairs[end].0 == pairs[start].0 {
344            end += 1;
345        }
346        let (a, b) = pairs[start].0;
347        let mid = midpoint(&positions[a as usize], &positions[b as usize]);
348        if in_region(&mid, region)
349            && dist(&positions[a as usize], &positions[b as usize]) > sigma * SPLIT_FACTOR
350            && pairs[start..end]
351                .iter()
352                .all(|&(_, i)| carries(&tris[i].corners, a, b))
353        {
354            let m = positions.len() as u32;
355            let parents = blend_parents(a, b, slots, added)?;
356            push(positions, mid)?;
357            push(vlive, true)?;
358            push(added, NewVert { pos: mid, parents })?;
359            for &(_, i) in &pairs[start..end] {
360                let corners = tris[i].corners;
361                let apex = apex_of(&corners, a, b);
362                // The children follow the parent's wound order around the
363                // edge; fixed-order children would flip every parent that
364                // winds b -> a.
365                let ab = runs_directed(&corners, a, b);
366                let (first, second) = if ab {
367                    ([a, m, apex], [m, b, apex])
368                } else {
369                    ([b, m, apex], [m, a, apex])
370                };
371                tris[i].corners = first;
372                push(
373                    tris,
374                    WorkTri {
375                        id: None,
376                        corners: second,
377                        alive: true,
378                    },
379                )?;
380            }
381            splits += 1;
382        }
383        start = end;
384    }
385    Ok(splits)
386}
387
388/// Collapses every short in-region interior edge once, merging its endpoints
389/// into one midpoint vertex. Edges created earlier in the same round are
390/// left alone so the pass cannot undo the split pass behind it. Returns the
391/// collapse count.
392fn collapse_round(
393    positions: &mut Vec<[f32; 3]>,
394    vlive: &mut Vec<bool>,
395    tris: &mut [WorkTri],
396    added: &mut Vec<NewVert>,
397    region: &Region,
398    sigma: f64,
399    fresh: usize,
400) -> Result<usize> {
401    let slots = positions.len() - added.len();
402    let pairs = snapshot_pairs(tris, vlive)?;
403    // Vertices touching the boundary (or a non-manifold edge) are locked:
404    // collapsing past them would reshape the boundary itself.
405    let mut locked: Vec<bool> = Vec::new();
406    reserve(&mut locked, vlive.len())?;
407    locked.extend(std::iter::repeat_n(false, vlive.len()));
408    let mut run = 0;
409    while run < pairs.len() {
410        let mut end = run + 1;
411        while end < pairs.len() && pairs[end].0 == pairs[run].0 {
412            end += 1;
413        }
414        if end - run != 2 {
415            locked[pairs[run].0.0 as usize] = true;
416            locked[pairs[run].0.1 as usize] = true;
417        }
418        run = end;
419    }
420    let mut collapses = 0;
421    let mut start = 0;
422    while start < pairs.len() {
423        let mut end = start + 1;
424        while end < pairs.len() && pairs[end].0 == pairs[start].0 {
425            end += 1;
426        }
427        let (a, b) = pairs[start].0;
428        if end - start == 2
429            && (a as usize) < fresh
430            && (b as usize) < fresh
431            && vlive[a as usize]
432            && vlive[b as usize]
433            && !locked[a as usize]
434            && !locked[b as usize]
435        {
436            let mid = midpoint(&positions[a as usize], &positions[b as usize]);
437            let short = matches!(
438                dist(&positions[a as usize], &positions[b as usize])
439                    .partial_cmp(&(sigma * COLLAPSE_FACTOR)),
440                Some(Ordering::Less)
441            );
442            if in_region(&mid, region)
443                && short
444                && let Some(plan) = validate_collapse(positions, vlive, tris, a, b)?
445            {
446                let m = positions.len() as u32;
447                let parents = blend_parents(a, b, slots, added)?;
448                push(positions, plan.mid)?;
449                push(vlive, true)?;
450                push(
451                    added,
452                    NewVert {
453                        pos: plan.mid,
454                        parents,
455                    },
456                )?;
457                tris[plan.owners[0]].alive = false;
458                tris[plan.owners[1]].alive = false;
459                for i in plan.fan {
460                    tris[i].corners = tris[i]
461                        .corners
462                        .map(|v| if v == a || v == b { m } else { v });
463                }
464                vlive[a as usize] = false;
465                vlive[b as usize] = false;
466                collapses += 1;
467            }
468        }
469        start = end;
470    }
471    Ok(collapses)
472}
473
474/// A validated collapse: the two owners to remove and the fan to repoint.
475struct CollapsePlan {
476    owners: [usize; 2],
477    fan: Vec<usize>,
478    mid: [f32; 3],
479}
480
481/// Validates collapsing `(a, b)` against the current working state: exactly
482/// two owners, the link condition (no duplicate triangles after the merge),
483/// and no folded fan triangle.
484fn validate_collapse(
485    positions: &[[f32; 3]],
486    vlive: &[bool],
487    tris: &[WorkTri],
488    a: u32,
489    b: u32,
490) -> Result<Option<CollapsePlan>> {
491    let mut owners: Vec<usize> = Vec::new();
492    let mut fan: Vec<usize> = Vec::new();
493    let mut na: Vec<u32> = Vec::new();
494    let mut nb: Vec<u32> = Vec::new();
495    for (i, tri) in tris.iter().enumerate() {
496        if !tri.alive {
497            continue;
498        }
499        let c = tri.corners;
500        if degenerate(&c) || !vlive[c[0] as usize] || !vlive[c[1] as usize] || !vlive[c[2] as usize]
501        {
502            // Skipped triangles are never repointed, so the collapse is
503            // only safe when none of them touches either endpoint.
504            if c.contains(&a) || c.contains(&b) {
505                return Ok(None);
506            }
507            continue;
508        }
509        let has_a = c.contains(&a);
510        let has_b = c.contains(&b);
511        if has_a && has_b {
512            push(&mut owners, i)?;
513        } else if has_a || has_b {
514            push(&mut fan, i)?;
515            let side = if has_a { &mut na } else { &mut nb };
516            for &v in &c {
517                if v != a && v != b {
518                    push(side, v)?;
519                }
520            }
521        }
522    }
523    if owners.len() != 2 {
524        return Ok(None);
525    }
526    let c = apex_of(&tris[owners[0]].corners, a, b);
527    let d = apex_of(&tris[owners[1]].corners, a, b);
528    push(&mut na, c)?;
529    push(&mut na, d)?;
530    push(&mut nb, c)?;
531    push(&mut nb, d)?;
532    na.sort_unstable();
533    na.dedup();
534    nb.sort_unstable();
535    nb.dedup();
536    let mut common: Vec<u32> = Vec::new();
537    for &v in &na {
538        if nb.binary_search(&v).is_ok() {
539            push(&mut common, v)?;
540        }
541    }
542    if common.as_slice() != [c.min(d), c.max(d)] {
543        return Ok(None);
544    }
545    let mid = midpoint(&positions[a as usize], &positions[b as usize]);
546    let m = f64v(&mid);
547    for &i in &fan {
548        let old = tris[i].corners;
549        let new = old.map(|v| {
550            if v == a || v == b {
551                m
552            } else {
553                f64v(&positions[v as usize])
554            }
555        });
556        let before = tri_normal(&old, positions);
557        let after = cross3(sub3(new[1], new[0]), sub3(new[2], new[0]));
558        if !matches!(
559            dot3(after, before).partial_cmp(&0.0),
560            Some(Ordering::Greater)
561        ) {
562            return Ok(None);
563        }
564    }
565    Ok(Some(CollapsePlan {
566        owners: [owners[0], owners[1]],
567        fan,
568        mid,
569    }))
570}
571
572/// Flips every interior edge whose quad gains a larger minimum angle,
573/// provided the replacement edge stays within the split band so flips never
574/// re-arm the split pass. Returns the flip count.
575fn flip_round(
576    positions: &[[f32; 3]],
577    vlive: &[bool],
578    tris: &mut [WorkTri],
579    region: &Region,
580    sigma: f64,
581) -> Result<usize> {
582    let pairs = snapshot_pairs(tris, vlive)?;
583    let mut unique: Vec<(u32, u32)> = Vec::new();
584    reserve(&mut unique, pairs.len())?;
585    unique.extend(pairs.iter().map(|&(e, _)| e));
586    unique.dedup();
587    let mut created: Vec<(u32, u32)> = Vec::new();
588    let mut flips = 0;
589    let mut start = 0;
590    while start < pairs.len() {
591        let mut end = start + 1;
592        while end < pairs.len() && pairs[end].0 == pairs[start].0 {
593            end += 1;
594        }
595        if end - start == 2 {
596            let (a, b) = pairs[start].0;
597            let mid = midpoint(&positions[a as usize], &positions[b as usize]);
598            let ctx = FlipCtx {
599                positions,
600                tris: &*tris,
601                vlive,
602                unique: &unique,
603                created: &created,
604                sigma,
605            };
606            if in_region(&mid, region)
607                && let Some(plan) = validate_flip(&ctx, a, b, pairs[start].1, pairs[start + 1].1)
608            {
609                tris[plan.owners[0]].corners = plan.new[0];
610                tris[plan.owners[1]].corners = plan.new[1];
611                push(&mut created, plan.created)?;
612                flips += 1;
613            }
614        }
615        start = end;
616    }
617    Ok(flips)
618}
619
620/// Shared inputs for [`validate_flip`].
621struct FlipCtx<'a> {
622    positions: &'a [[f32; 3]],
623    tris: &'a [WorkTri],
624    vlive: &'a [bool],
625    unique: &'a [(u32, u32)],
626    created: &'a [(u32, u32)],
627    sigma: f64,
628}
629
630/// A validated flip: the two owners and their replacement corners.
631struct FlipPlan {
632    owners: [usize; 2],
633    new: [[u32; 3]; 2],
634    created: (u32, u32),
635}
636
637/// Validates flipping `(a, b)` with owners `o1`/`o2` against the current
638/// working state: both owners intact, distinct apexes, no duplicate edge,
639/// a shorter-or-equal replacement, a strictly larger minimum angle, and no
640/// folded triangle.
641fn validate_flip(ctx: &FlipCtx, a: u32, b: u32, o1: usize, o2: usize) -> Option<FlipPlan> {
642    for &o in &[o1, o2] {
643        let t = &ctx.tris[o];
644        if !t.alive || degenerate(&t.corners) || !carries(&t.corners, a, b) {
645            return None;
646        }
647        if !ctx.vlive[t.corners[0] as usize]
648            || !ctx.vlive[t.corners[1] as usize]
649            || !ctx.vlive[t.corners[2] as usize]
650        {
651            return None;
652        }
653    }
654    let old1 = ctx.tris[o1].corners;
655    let old2 = ctx.tris[o2].corners;
656    let c = apex_of(&old1, a, b);
657    let d = apex_of(&old2, a, b);
658    if c == d {
659        return None;
660    }
661    // The new triangles follow the owners' wound order around the edge, so
662    // the new diagonal is used in opposite directions by construction: a
663    // normal-based guess (each triangle aligned to its old one) agrees on
664    // flat quads but sprays conflicts on curved ones. An edge the owners
665    // already use the same way cannot be fixed by flipping: leave it.
666    let ab1 = runs_directed(&old1, a, b);
667    if ab1 == runs_directed(&old2, a, b) {
668        return None;
669    }
670    let (new1, new2) = if ab1 {
671        ([c, a, d], [c, d, b])
672    } else {
673        ([c, d, a], [d, c, b])
674    };
675    let e = (c.min(d), c.max(d));
676    if ctx.unique.binary_search(&e).is_ok() || ctx.created.contains(&e) {
677        return None;
678    }
679    let replacement = dist(&ctx.positions[c as usize], &ctx.positions[d as usize]);
680    if !matches!(
681        replacement.partial_cmp(&(ctx.sigma * SPLIT_FACTOR)),
682        Some(Ordering::Less | Ordering::Equal)
683    ) {
684        return None;
685    }
686    // Area conservation: a valid flip retriangulates the same quad, so the
687    // total area only shifts at second order in curvature, while nested or
688    // overlapping new triangles inflate it by the double-covered part. 1%
689    // separates the two with wide margins either way; sub-1% grazing
690    // overlaps are the accepted residual (winding stays consistent).
691    let area_old = doubled_area(&old1, ctx.positions) + doubled_area(&old2, ctx.positions);
692    let area_new = doubled_area(&new1, ctx.positions) + doubled_area(&new2, ctx.positions);
693    if !matches!(
694        area_new.partial_cmp(&(area_old * 1.01)),
695        Some(Ordering::Less | Ordering::Equal)
696    ) {
697        return None;
698    }
699    let old_max = match (
700        max_cos_angle(&old1, ctx.positions),
701        max_cos_angle(&old2, ctx.positions),
702    ) {
703        (Some(x), Some(y)) => x.max(y),
704        _ => return None,
705    };
706    let new_max = match (
707        max_cos_angle(&new1, ctx.positions),
708        max_cos_angle(&new2, ctx.positions),
709    ) {
710        (Some(x), Some(y)) => x.max(y),
711        _ => return None,
712    };
713    if !matches!(new_max.partial_cmp(&old_max), Some(Ordering::Less)) {
714        return None;
715    }
716    // The winding is exact by construction; the normals only reject folds.
717    if dot3(
718        tri_normal(&new1, ctx.positions),
719        tri_normal(&old1, ctx.positions),
720    ) <= 0.0
721        || dot3(
722            tri_normal(&new2, ctx.positions),
723            tri_normal(&old2, ctx.positions),
724        ) <= 0.0
725    {
726        return None;
727    }
728    Some(FlipPlan {
729        owners: [o1, o2],
730        new: [new1, new2],
731        created: e,
732    })
733}
734
735/// Whether `corners` winds the edge `a -> b` (as opposed to `b -> a`).
736pub(crate) fn runs_directed(corners: &[u32; 3], a: u32, b: u32) -> bool {
737    (corners[0] == a && corners[1] == b)
738        || (corners[1] == a && corners[2] == b)
739        || (corners[2] == a && corners[0] == b)
740}
741
742/// Every live triangle's edges with their owner index, sorted by edge.
743fn snapshot_pairs(tris: &[WorkTri], vlive: &[bool]) -> Result<Vec<((u32, u32), usize)>> {
744    let mut pairs: Vec<((u32, u32), usize)> = Vec::new();
745    reserve(&mut pairs, 3 * tris.len())?;
746    for (i, tri) in tris.iter().enumerate() {
747        if !tri.alive || degenerate(&tri.corners) {
748            continue;
749        }
750        let c = tri.corners;
751        if !vlive[c[0] as usize] || !vlive[c[1] as usize] || !vlive[c[2] as usize] {
752            continue;
753        }
754        for k in 0..3 {
755            let (a, b) = (c[k], c[(k + 1) % 3]);
756            push(&mut pairs, ((a.min(b), a.max(b)), i))?;
757        }
758    }
759    pairs.sort_unstable();
760    Ok(pairs)
761}
762
763pub(crate) fn degenerate(corners: &[u32; 3]) -> bool {
764    corners[0] == corners[1] || corners[1] == corners[2] || corners[2] == corners[0]
765}
766
767pub(crate) fn carries(corners: &[u32; 3], a: u32, b: u32) -> bool {
768    corners.contains(&a) && corners.contains(&b)
769}
770
771/// The corner of a triangle carrying `(a, b)` that is neither endpoint.
772pub(crate) fn apex_of(corners: &[u32; 3], a: u32, b: u32) -> u32 {
773    if corners[0] != a && corners[0] != b {
774        corners[0]
775    } else if corners[1] != a && corners[1] != b {
776        corners[1]
777    } else {
778        corners[2]
779    }
780}
781
782pub(crate) fn f64v(p: &[f32; 3]) -> [f64; 3] {
783    [f64::from(p[0]), f64::from(p[1]), f64::from(p[2])]
784}
785
786pub(crate) fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
787    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
788}
789
790pub(crate) fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
791    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
792}
793
794pub(crate) fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
795    [
796        a[1] * b[2] - a[2] * b[1],
797        a[2] * b[0] - a[0] * b[2],
798        a[0] * b[1] - a[1] * b[0],
799    ]
800}
801
802fn tri_normal(corners: &[u32; 3], positions: &[[f32; 3]]) -> [f64; 3] {
803    let p0 = f64v(&positions[corners[0] as usize]);
804    let p1 = f64v(&positions[corners[1] as usize]);
805    let p2 = f64v(&positions[corners[2] as usize]);
806    cross3(sub3(p1, p0), sub3(p2, p0))
807}
808
809/// Twice the area of a triangle (the length of its cross product).
810fn doubled_area(corners: &[u32; 3], positions: &[[f32; 3]]) -> f64 {
811    let n = tri_normal(corners, positions);
812    dot3(n, n).sqrt()
813}
814
815/// The largest corner cosine of a triangle (its smallest angle, as a
816/// cosine), or `None` when an edge has zero length.
817fn max_cos_angle(corners: &[u32; 3], positions: &[[f32; 3]]) -> Option<f64> {
818    let p = [
819        f64v(&positions[corners[0] as usize]),
820        f64v(&positions[corners[1] as usize]),
821        f64v(&positions[corners[2] as usize]),
822    ];
823    let mut max = f64::NEG_INFINITY;
824    for k in 0..3 {
825        let u = sub3(p[k], p[(k + 1) % 3]);
826        let v = sub3(p[k], p[(k + 2) % 3]);
827        let (nu, nv) = (dot3(u, u).sqrt(), dot3(v, v).sqrt());
828        if nu == 0.0 || nv == 0.0 {
829            return None;
830        }
831        max = max.max(dot3(u, v) / (nu * nv));
832    }
833    Some(max)
834}
835
836/// Weights a midpoint over live base vertices. Endpoints created by earlier
837/// rounds contribute their own weights halved, merged by vertex id, so the
838/// weights always describe the exact midpoint over the base mesh.
839pub(crate) fn blend_parents(
840    a: u32,
841    b: u32,
842    slots: usize,
843    added: &[NewVert],
844) -> Result<Vec<(VertexId, f32)>> {
845    let mut entries: Vec<(u32, f32)> = Vec::new();
846    for v in [a, b] {
847        if (v as usize) < slots {
848            push(&mut entries, (v, 0.5))?;
849        } else {
850            for &(id, w) in &added[(v as usize) - slots].parents {
851                push(&mut entries, (id, 0.5 * w))?;
852            }
853        }
854    }
855    entries.sort_unstable_by_key(|x| x.0);
856    let mut parents: Vec<(VertexId, f32)> = Vec::new();
857    reserve(&mut parents, entries.len())?;
858    for (id, w) in entries {
859        match parents.last_mut() {
860            Some(last) if last.0 == id => last.1 += w,
861            _ => push(&mut parents, (id, w))?,
862        }
863    }
864    Ok(parents)
865}
866
867fn commit(
868    tx: &mut Transaction,
869    slots: usize,
870    vlive: &[bool],
871    tris: &[WorkTri],
872    added: &[NewVert],
873) -> Result<()> {
874    // Everything is reserved before the first write: past this point only
875    // infallible pushes into reserved capacity remain, so any allocation
876    // failure still leaves an empty patch.
877    let mut n_old = 0;
878    let mut n_new = 0;
879    let mut n_dead_tri = 0;
880    for tri in tris {
881        match tri.id {
882            Some(_) if !tri.alive => n_dead_tri += 1,
883            Some(_) => n_old += 1,
884            None if tri.alive => n_new += 1,
885            None => {}
886        }
887    }
888    let n_dead_vert = vlive.iter().take(slots).filter(|&&live| !live).count();
889    let mut ids: Vec<VertexId> = Vec::new();
890    reserve(&mut ids, added.len())?;
891    let mut set_ids: Vec<TriangleId> = Vec::new();
892    reserve(&mut set_ids, n_old)?;
893    let mut set_tris: Vec<[VertexId; 3]> = Vec::new();
894    reserve(&mut set_tris, n_old)?;
895    let mut new_tris: Vec<[VertexId; 3]> = Vec::new();
896    reserve(&mut new_tris, n_new)?;
897    let mut del_tris: Vec<TriangleId> = Vec::new();
898    reserve(&mut del_tris, n_dead_tri)?;
899    let mut del_verts: Vec<VertexId> = Vec::new();
900    reserve(&mut del_verts, n_dead_vert)?;
901    for (k, nv) in added.iter().enumerate() {
902        if vlive[slots + k] {
903            ids.push(tx.add_vertex_with_parents(nv.pos, &nv.parents)?);
904        } else {
905            // Collapsed before commit; no live triangle references it.
906            ids.push(VertexId::MAX);
907        }
908    }
909    let resolve = |v: u32| -> VertexId {
910        debug_assert!(vlive[v as usize]);
911        let i = v as usize;
912        if i < slots { v } else { ids[i - slots] }
913    };
914    let mesh = tx.mesh();
915    for tri in tris {
916        match tri.id {
917            Some(id) if !tri.alive => del_tris.push(id),
918            Some(id) => {
919                let current = [
920                    resolve(tri.corners[0]),
921                    resolve(tri.corners[1]),
922                    resolve(tri.corners[2]),
923                ];
924                let original = mesh.triangle(id).expect("remesh: working id must exist");
925                if current != original {
926                    set_ids.push(id);
927                    set_tris.push(current);
928                }
929            }
930            None if tri.alive => new_tris.push([
931                resolve(tri.corners[0]),
932                resolve(tri.corners[1]),
933                resolve(tri.corners[2]),
934            ]),
935            None => {}
936        }
937    }
938    for v in 0..slots as u32 {
939        if !vlive[v as usize] {
940            del_verts.push(v);
941        }
942    }
943    tx.set_triangles(&set_ids, &set_tris)?;
944    tx.add_triangles(&new_tris)?;
945    tx.remove_triangles(&del_tris)?;
946    tx.remove_vertices(&del_verts)?;
947    Ok(())
948}
949
950#[cfg(test)]
951mod tests {
952    use fast_mesh_store::{Mesh, Transaction, VertexId};
953
954    use super::*;
955    use crate::OpsError;
956    use crate::test_util::{edit, mesh_of, parts, sweep, with_triangles};
957    use crate::topology::{EdgeMap, point, sub};
958
959    fn quad_4x1() -> Mesh {
960        mesh_of(
961            &[0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 4.0, 1.0, 0.0, 0.0, 1.0, 0.0],
962            &[[0, 1, 2], [0, 2, 3]],
963        )
964    }
965
966    fn edge_lengths(mesh: &Mesh) -> Vec<f64> {
967        let edges = EdgeMap::build(mesh).unwrap();
968        edges
969            .edges()
970            .map(|((a, b), _)| {
971                let d = sub(point(mesh, a), point(mesh, b));
972                (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
973            })
974            .collect()
975    }
976
977    fn all_region() -> RemeshOptions {
978        RemeshOptions::builder()
979            .center([2.0, 0.5, 0.0])
980            .radius(10.0)
981            .target_length(1.0)
982            .build()
983    }
984
985    /// Every live triangle faces +z with positive area (planar fixtures).
986    fn assert_faces_up(mesh: &Mesh) {
987        let (pos, tris) = parts(mesh);
988        assert!(!tris.is_empty());
989        for t in &tris {
990            let x = |v: u32| f64::from(pos[3 * v as usize]);
991            let y = |v: u32| f64::from(pos[3 * v as usize + 1]);
992            let z = (x(t[1]) - x(t[0])) * (y(t[2]) - y(t[0]))
993                - (y(t[1]) - y(t[0])) * (x(t[2]) - x(t[0]));
994            assert!(z > 0.0, "flipped or degenerate: {t:?} z={z}");
995        }
996    }
997
998    #[test]
999    fn split_refines_sparse_quad_to_target() {
1000        let mut mesh = quad_4x1();
1001        let before = parts(&mesh);
1002        let (report, _) = edit(&mut mesh, |tx| remesh(tx, &all_region()));
1003        assert!(report.splits > 0, "expected splits, got {report:?}");
1004        for len in edge_lengths(&mesh) {
1005            assert!(len <= 4.0 / 3.0 + 1e-4, "long edge left: {len}");
1006        }
1007        let (pos_after, tris_after) = parts(&mesh);
1008        assert_eq!(&pos_after[..12], &before.0[..12]);
1009        assert!(tris_after.len() > before.1.len());
1010        assert_faces_up(&mesh);
1011    }
1012
1013    #[test]
1014    fn split_empty_region_and_coarse_target() {
1015        let mut mesh = quad_4x1();
1016        let before = parts(&mesh);
1017        let far = RemeshOptions::builder()
1018            .center([100.0, 0.0, 0.0])
1019            .radius(1.0)
1020            .target_length(1.0)
1021            .build();
1022        let (report, empty) = edit(&mut mesh, |tx| remesh(tx, &far));
1023        assert_eq!((report.splits, report.collapses, report.flips), (0, 0, 0));
1024        assert!(empty);
1025        assert_eq!(parts(&mesh), before);
1026        let mut mesh = quad_4x1();
1027        let coarse = RemeshOptions::builder()
1028            .center([2.0, 0.5, 0.0])
1029            .radius(10.0)
1030            .target_length(100.0)
1031            .build();
1032        let (report, _) = edit(&mut mesh, |tx| remesh(tx, &coarse));
1033        assert_eq!(
1034            (report.splits, report.collapses, report.flips, report.rounds),
1035            (0, 0, 0, 0)
1036        );
1037    }
1038
1039    #[test]
1040    fn split_rejects_invalid_region() {
1041        let mut mesh = quad_4x1();
1042        let zero = RemeshOptions::builder()
1043            .center([2.0, 0.5, 0.0])
1044            .radius(0.0)
1045            .target_length(1.0)
1046            .build();
1047        {
1048            let mut tx = mesh.begin_edit();
1049            assert_eq!(
1050                remesh(&mut tx, &zero),
1051                Err(OpsError::InvalidInput(
1052                    "remesh region radius must be positive and finite"
1053                ))
1054            );
1055        }
1056        let nan = RemeshOptions::builder()
1057            .center([f32::NAN, 0.0, 0.0])
1058            .radius(10.0)
1059            .target_length(1.0)
1060            .build();
1061        {
1062            let mut tx = mesh.begin_edit();
1063            assert_eq!(
1064                remesh(&mut tx, &nan),
1065                Err(OpsError::InvalidInput(
1066                    "remesh region center must be finite"
1067                ))
1068            );
1069        }
1070    }
1071
1072    #[test]
1073    fn split_keeps_boundary_vertices_live() {
1074        let mut mesh = quad_4x1();
1075        edit(&mut mesh, |tx| remesh(tx, &all_region()));
1076        for v in 0..4u32 {
1077            assert!(mesh.is_vertex_alive(v as VertexId));
1078        }
1079    }
1080
1081    #[test]
1082    fn split_sweep_keeps_atomicity() {
1083        let n = sweep(quad_4x1(), |tx: &mut Transaction| {
1084            remesh(tx, &all_region())?;
1085            Ok(())
1086        });
1087        assert!(n > 0);
1088    }
1089
1090    fn grid_4x4() -> Mesh {
1091        let mut pos = Vec::new();
1092        for j in 0..4 {
1093            for i in 0..4 {
1094                pos.extend([i as f32 * 0.1, j as f32 * 0.1, 0.0]);
1095            }
1096        }
1097        let mut tris = Vec::new();
1098        for j in 0..3 {
1099            for i in 0..3 {
1100                let a = j * 4 + i;
1101                tris.push([a, a + 1, a + 5]);
1102                tris.push([a, a + 5, a + 4]);
1103            }
1104        }
1105        mesh_of(&pos, &tris)
1106    }
1107
1108    fn kite() -> Mesh {
1109        // Convex kite, apexes on opposite sides of the shared edge (0, 1):
1110        // (0, 1, 2) is skinny, so flipping to (2, 3) improves the angles.
1111        mesh_of(
1112            &[0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 1.0, 0.15, 0.0, 1.0, -2.0, 0.0],
1113            &[[0, 1, 2], [1, 0, 3]],
1114        )
1115    }
1116
1117    #[test]
1118    fn collapse_coarsens_dense_interior() {
1119        let mut mesh = grid_4x4();
1120        let before = parts(&mesh);
1121        let options = RemeshOptions::builder()
1122            .center([0.15, 0.15, 0.0])
1123            .radius(10.0)
1124            .target_length(1.0)
1125            .build();
1126        let (report, _) = edit(&mut mesh, |tx| remesh(tx, &options));
1127        assert_eq!(report.splits, 0);
1128        assert!(report.collapses > 0, "expected collapses, got {report:?}");
1129        let (_, tris_after) = parts(&mesh);
1130        assert!(tris_after.len() < before.1.len());
1131        for t in &tris_after {
1132            assert!(
1133                t[0] != t[1] && t[1] != t[2] && t[2] != t[0],
1134                "degenerate: {t:?}"
1135            );
1136        }
1137        for v in [0, 1, 2, 3, 4, 7, 8, 11, 12, 13, 14, 15] {
1138            assert!(mesh.is_vertex_alive(v as VertexId));
1139        }
1140        assert_faces_up(&mesh);
1141    }
1142
1143    #[test]
1144    fn collapse_skips_all_boundary() {
1145        let mut mesh = mesh_of(&[0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 3.0, 0.0], &[[0, 1, 2]]);
1146        let before = parts(&mesh);
1147        let options = RemeshOptions::builder()
1148            .center([1.0, 1.0, 0.0])
1149            .radius(10.0)
1150            .target_length(10.0)
1151            .build();
1152        let (report, empty) = edit(&mut mesh, |tx| remesh(tx, &options));
1153        assert_eq!(
1154            (report.splits, report.collapses, report.flips, report.rounds),
1155            (0, 0, 0, 0)
1156        );
1157        assert!(empty);
1158        assert_eq!(parts(&mesh), before);
1159    }
1160
1161    #[test]
1162    fn flip_improves_skinny_kite() {
1163        let mut mesh = kite();
1164        let options = RemeshOptions::builder()
1165            .center([1.0, 0.5, 0.0])
1166            .radius(10.0)
1167            .target_length(10.0)
1168            .build();
1169        let (report, _) = edit(&mut mesh, |tx| remesh(tx, &options));
1170        assert_eq!(
1171            (report.splits, report.collapses, report.flips, report.rounds),
1172            (0, 0, 1, 1)
1173        );
1174        let (_, tris) = parts(&mesh);
1175        assert!(
1176            tris.iter().any(|t| t.contains(&2) && t.contains(&3)),
1177            "flipped diagonal missing: {tris:?}"
1178        );
1179        assert_faces_up(&mesh);
1180    }
1181
1182    #[test]
1183    fn remesh_split_only_never_collapses_or_flips() {
1184        // A dense interior a full pass would coarsen: split-only leaves it alone.
1185        let mut mesh = grid_4x4();
1186        let before = parts(&mesh);
1187        let options = RemeshOptions::builder()
1188            .center([0.15, 0.15, 0.0])
1189            .radius(10.0)
1190            .target_length(1.0)
1191            .split_only(true)
1192            .build();
1193        let (report, empty) = edit(&mut mesh, |tx| remesh(tx, &options));
1194        assert_eq!(
1195            (report.splits, report.collapses, report.flips, report.rounds),
1196            (0, 0, 0, 0)
1197        );
1198        assert!(empty);
1199        assert_eq!(parts(&mesh), before);
1200        // A sparse quad a full pass would refine: split-only still splits.
1201        let mut mesh = quad_4x1();
1202        let options = RemeshOptions::builder()
1203            .center([2.0, 0.5, 0.0])
1204            .radius(10.0)
1205            .target_length(1.0)
1206            .split_only(true)
1207            .build();
1208        let (report, _) = edit(&mut mesh, |tx| remesh(tx, &options));
1209        assert!(report.splits > 0, "expected splits, got {report:?}");
1210        assert_eq!((report.collapses, report.flips), (0, 0));
1211    }
1212
1213    #[test]
1214    fn remesh_respects_max_rounds() {
1215        let mut mesh = quad_4x1();
1216        let before = parts(&mesh);
1217        let options = RemeshOptions::builder()
1218            .center([2.0, 0.5, 0.0])
1219            .radius(10.0)
1220            .target_length(1.0)
1221            .max_rounds(0)
1222            .build();
1223        let (report, empty) = edit(&mut mesh, |tx| remesh(tx, &options));
1224        assert_eq!(
1225            (report.splits, report.collapses, report.flips, report.rounds),
1226            (0, 0, 0, 0)
1227        );
1228        assert!(empty);
1229        assert_eq!(parts(&mesh), before);
1230    }
1231
1232    #[test]
1233    fn collapse_and_flip_keep_atomicity() {
1234        let grid_options = RemeshOptions::builder()
1235            .center([0.15, 0.15, 0.0])
1236            .radius(10.0)
1237            .target_length(1.0)
1238            .build();
1239        let n = sweep(grid_4x4(), |tx: &mut Transaction| {
1240            remesh(tx, &grid_options)?;
1241            Ok(())
1242        });
1243        assert!(n > 0);
1244        let kite_options = RemeshOptions::builder()
1245            .center([1.0, 0.5, 0.0])
1246            .radius(10.0)
1247            .target_length(10.0)
1248            .build();
1249        let n = sweep(kite(), |tx: &mut Transaction| {
1250            remesh(tx, &kite_options)?;
1251            Ok(())
1252        });
1253        assert!(n > 0);
1254    }
1255
1256    #[test]
1257    fn collapse_skips_edges_touching_degenerate_triangles() {
1258        // Degenerate (5, 5, 9) touches interior vertex 5 but not edge (5, 6):
1259        // that collapse is skipped, the rest of the grid still coarsens, and
1260        // the degenerate triangle is left untouched.
1261        let mut mesh = with_triangles(&grid_4x4(), &[[5, 5, 9]]);
1262        let options = RemeshOptions::builder()
1263            .center([0.15, 0.15, 0.0])
1264            .radius(10.0)
1265            .target_length(1.0)
1266            .build();
1267        let (report, _) = edit(&mut mesh, |tx| remesh(tx, &options));
1268        assert!(report.collapses > 0, "expected collapses, got {report:?}");
1269        let (_, tris) = parts(&mesh);
1270        assert!(tris.contains(&[5, 5, 9]));
1271        for t in &tris {
1272            for &v in t {
1273                assert!(mesh.is_vertex_alive(v));
1274            }
1275        }
1276    }
1277
1278    #[test]
1279    fn flip_rejects_reflex_quad() {
1280        // Reflex at c=(0, 1): the replacement diagonal (c, d) would leave
1281        // the quad, nesting one new triangle inside the other.
1282        let mut mesh = mesh_of(
1283            &[1.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, -1.0, 0.0],
1284            &[[0, 1, 2], [1, 0, 3]],
1285        );
1286        let before = parts(&mesh);
1287        let options = RemeshOptions::builder()
1288            .center([1.0, 0.0, 0.0])
1289            .radius(10.0)
1290            .target_length(10.0)
1291            .build();
1292        let (report, empty) = edit(&mut mesh, |tx| remesh(tx, &options));
1293        assert_eq!(
1294            (report.splits, report.collapses, report.flips, report.rounds),
1295            (0, 0, 0, 0)
1296        );
1297        assert!(empty);
1298        assert_eq!(parts(&mesh), before);
1299    }
1300
1301    #[test]
1302    fn split_finds_edge_crossing_vertex_free_region() {
1303        // The region around (5, 0) holds no vertices, but the long base
1304        // edge crosses it: its midpoint is in, so it splits.
1305        let mut mesh = mesh_of(
1306            &[0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 0.1, 0.0],
1307            &[[0, 1, 2]],
1308        );
1309        let options = RemeshOptions::builder()
1310            .center([5.0, 0.0, 0.0])
1311            .radius(1.0)
1312            .target_length(1.0)
1313            .build();
1314        let (report, _) = edit(&mut mesh, |tx| remesh(tx, &options));
1315        assert!(report.splits > 0, "expected splits, got {report:?}");
1316    }
1317
1318    fn grid_10x10() -> Mesh {
1319        let mut pos = Vec::new();
1320        for j in 0..10 {
1321            for i in 0..10 {
1322                pos.extend([i as f32 * 0.2, j as f32 * 0.2, 0.0]);
1323            }
1324        }
1325        let mut tris = Vec::new();
1326        for j in 0..9 {
1327            for i in 0..9 {
1328                let a = j * 10 + i;
1329                tris.push([a, a + 1, a + 11]);
1330                tris.push([a, a + 11, a + 10]);
1331            }
1332        }
1333        mesh_of(&pos, &tris)
1334    }
1335
1336    #[test]
1337    fn remesh_after_inflate_keeps_orientation() {
1338        use crate::analyze::{AnalyzeOptions, analyze};
1339        use crate::deform::{Falloff, InflateOptions, inflate};
1340
1341        let mut mesh = grid_10x10();
1342        for _ in 0..4 {
1343            edit(&mut mesh, |tx| {
1344                inflate(
1345                    tx,
1346                    &InflateOptions::builder()
1347                        .center([0.9, 0.9, 0.0])
1348                        .radius(0.8)
1349                        .falloff(Falloff::Smooth)
1350                        .strength(0.2)
1351                        .build(),
1352                )
1353                .map(drop)
1354            });
1355        }
1356        let options = RemeshOptions::builder()
1357            .center([0.9, 0.9, 0.0])
1358            .radius(1.2)
1359            .max_rounds(4)
1360            .build();
1361        let (report, _) = edit(&mut mesh, |tx| remesh(tx, &options));
1362        assert!(
1363            report.splits > 0 && report.flips > 0,
1364            "must exercise splits and flips, got {report:?}"
1365        );
1366        let a = analyze(&mesh, &AnalyzeOptions::default()).unwrap();
1367        assert_eq!(a.orientation_conflicts, 0);
1368    }
1369
1370    #[test]
1371    fn remesh_after_inflate_keeps_orientation_on_sphere() {
1372        use crate::analyze::{AnalyzeOptions, analyze};
1373        use crate::deform::{Falloff, InflateOptions, inflate};
1374        use crate::test_util::uv_sphere;
1375
1376        let mut mesh = uv_sphere(24, 36, 1.0);
1377        for _ in 0..6 {
1378            edit(&mut mesh, |tx| {
1379                inflate(
1380                    tx,
1381                    &InflateOptions::builder()
1382                        .center([0.0, 0.0, 1.0])
1383                        .radius(0.5)
1384                        .falloff(Falloff::Smooth)
1385                        .strength(0.1)
1386                        .build(),
1387                )
1388                .map(drop)
1389            });
1390        }
1391        let options = RemeshOptions::builder()
1392            .center([0.0, 0.0, 1.0])
1393            .radius(1.0)
1394            .max_rounds(4)
1395            .build();
1396        let (report, _) = edit(&mut mesh, |tx| remesh(tx, &options));
1397        assert!(
1398            report.splits > 0 && report.flips > 0,
1399            "must exercise splits and flips, got {report:?}"
1400        );
1401        let a = analyze(&mesh, &AnalyzeOptions::default()).unwrap();
1402        assert_eq!(a.orientation_conflicts, 0);
1403    }
1404
1405    #[test]
1406    fn remesh_after_inflate_keeps_orientation_on_noisy_sphere() {
1407        use crate::analyze::{AnalyzeOptions, analyze};
1408        use crate::deform::{Falloff, InflateOptions, inflate};
1409        use crate::test_util::{mesh_of, parts, uv_sphere};
1410
1411        // A blobby stand-in: deterministic high-frequency noise on a sphere.
1412        let (pos, tris) = parts(&uv_sphere(24, 36, 1.0));
1413        let noisy: Vec<f32> = pos
1414            .iter()
1415            .enumerate()
1416            .map(|(i, &p)| p + 0.02 * ((i as f32 * 12.9898).sin() + (i as f32 * 78.233).sin()))
1417            .collect();
1418        let mut mesh = mesh_of(&noisy, &tris);
1419        for _ in 0..6 {
1420            edit(&mut mesh, |tx| {
1421                inflate(
1422                    tx,
1423                    &InflateOptions::builder()
1424                        .center([0.0, 0.0, 1.0])
1425                        .radius(0.5)
1426                        .falloff(Falloff::Smooth)
1427                        .strength(0.1)
1428                        .build(),
1429                )
1430                .map(drop)
1431            });
1432        }
1433        let options = RemeshOptions::builder()
1434            .center([0.0, 0.0, 1.0])
1435            .radius(1.0)
1436            .max_rounds(4)
1437            .build();
1438        let (report, _) = edit(&mut mesh, |tx| remesh(tx, &options));
1439        assert!(
1440            report.splits > 0 && report.flips > 0,
1441            "must exercise splits and flips, got {report:?}"
1442        );
1443        let a = analyze(&mesh, &AnalyzeOptions::default()).unwrap();
1444        assert_eq!(a.orientation_conflicts, 0);
1445    }
1446
1447    #[test]
1448    fn split_defaults_target_to_mesh_mean() {
1449        // Mean edge length is (4 + 4 + 1 + 1 + sqrt(17)) / 5 ~ 2.82; the
1450        // two long sides and the diagonal exceed 4/3 of it exactly once.
1451        let mut mesh = quad_4x1();
1452        let options = RemeshOptions::builder()
1453            .center([2.0, 0.5, 0.0])
1454            .radius(10.0)
1455            .build();
1456        let (report, _) = edit(&mut mesh, |tx| remesh(tx, &options));
1457        assert_eq!(
1458            (report.splits, report.collapses, report.flips, report.rounds),
1459            (3, 0, 1, 2)
1460        );
1461        for len in edge_lengths(&mesh) {
1462            assert!(len <= 2.8247 * 4.0 / 3.0 + 1e-4, "long edge left: {len}");
1463        }
1464    }
1465}