Skip to main content

fast_mesh_scene/
sections.rs

1//! Plane sections of meshes of the scene (spec §6, §11).
2//!
3//! A section cuts the meshes it selects with one world [`Plane`]. For each
4//! selected mesh it keeps a [`SectionResult`]: the contours of the cut in
5//! world and plane `(u, v)` coordinates, their lengths and signed areas, and
6//! optionally the filled cross-section.
7//!
8//! **What an update recomputes.** Each section has a change accumulator on
9//! every mesh it selects (a [`SectionHook`], fed like a subscriber's report).
10//! An entry is stale when the hook holds a non-empty change set, "everything"
11//! or a placement change, or when the section's plane or fill changed, or the
12//! mesh was just selected. A stale entry is recomputed in full: the world
13//! plane is mapped into the mesh's local frame, cut with [`slice()`], and the
14//! points are mapped back.
15//!
16//! **Edits that cannot reach the plane** (spec §6.3, §11.5). Each entry
17//! keeps the sorted vertex ids of its crossed edges and its crossed
18//! triangles. An edit, undo or redo leaves the entry current, with nothing
19//! recomputed or reported, unless one of these holds for the changes merged
20//! since the last computation:
21//! 1. a moved or removed vertex is a crossed-edge vertex;
22//! 2. a removed or rewritten triangle is a crossed triangle;
23//! 3. a moved or added vertex that is not a crossed-edge vertex has an alive
24//!    incident triangle with vertices on both sides;
25//! 4. an added or rewritten triangle is alive and has vertices on both sides.
26//!
27//! Sides use the cut's own rule (`s ≥ 0` is `+`, from the stored `f32`
28//! positions) against the same local plane. Before the edit no edge at a
29//! vertex outside the crossed set crossed the plane, so these cover every
30//! segment and crossing point the cut depends on. A placement, plane or fill
31//! change, compaction and "everything" always recompute.
32
33use fast_mesh_query::{Fill, Gap2, Plane, Slice, contour_gap, contour_length, signed_area};
34use fast_mesh_store::{ChangeSet, Mesh, StoreError, TriangleId, VertexId, par};
35
36use crate::placement::{Placement, local_to_world, world_to_local};
37use crate::scene::Entry;
38use crate::{MeshId, Result, Scene, SceneError, SectionId, UpdateMode};
39
40/// A plane section to register with
41/// [`Scene::add_section`](crate::Scene::add_section).
42#[derive(Debug, Clone, PartialEq)]
43pub struct SectionDesc {
44    /// The cutting plane, in world coordinates. Its `(u, v)` frame is the
45    /// frame of every result's 2D points.
46    pub plane: Plane,
47    /// The meshes cut, each once.
48    pub meshes: Vec<MeshId>,
49    /// Whether results carry the filled cross-section.
50    pub fill: bool,
51}
52
53/// Whether a section's result for a mesh matches the scene as it is now,
54/// from [`Scene::section_status`](crate::Scene::section_status).
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum SectionStatus {
57    /// Computed, and nothing it depends on changed since.
58    Current,
59    /// Not computed yet, or something it depends on changed since.
60    Stale,
61}
62
63/// One chain of a section: a closed loop or an open polyline.
64#[derive(Debug, Clone, Default, PartialEq)]
65pub struct SectionContour {
66    /// The crossing point of each edge in `edges`, in world coordinates.
67    pub points: Vec<[f64; 3]>,
68    /// The same points in the section plane's `(u, v)` frame.
69    pub uv: Vec<[f64; 2]>,
70    /// The crossed edge `(min id, max id)` of each point.
71    pub edges: Vec<(VertexId, VertexId)>,
72    /// The triangle of each segment: `triangles[i]` joins point `i` to point
73    /// `i + 1`, and the last one of a closed contour joins the last point to
74    /// the first.
75    pub triangles: Vec<TriangleId>,
76    /// Whether the last point joins back to the first.
77    pub closed: bool,
78    /// Whether every segment runs the same way along the contour. On a
79    /// closed, outward-wound mesh outer loops run counter-clockwise in
80    /// `(u, v)` and holes clockwise.
81    pub oriented: bool,
82    /// Whether no end of the contour is a split at a non-manifold edge.
83    pub manifold: bool,
84    /// The length of the polyline (with the closing segment when closed).
85    pub length: f64,
86    /// The signed area in `(u, v)`, positive counter-clockwise; 0 when open.
87    pub area: f64,
88}
89
90/// A section's result for one mesh, from
91/// [`Scene::section`](crate::Scene::section).
92#[derive(Debug, Clone, PartialEq)]
93pub struct SectionResult {
94    /// The contours, sorted by their first edge key.
95    pub contours: Vec<SectionContour>,
96    /// The summed length of every contour.
97    pub length: f64,
98    /// The summed signed area of the closed, oriented contours.
99    pub area: f64,
100    /// The filled cross-section over the closed contours, when the section
101    /// has fill on and the result was computed. Its points are `(u, v)`;
102    /// [`Plane::from_uv`] of the section plane maps them to the world.
103    pub fill: Option<Fill>,
104    /// Whether the fill looks good (see [`fast_mesh_query::fill`]): best
105    /// effort, a bad fill can go unreported. `true` when `fill` is `None`.
106    pub fill_ok: bool,
107}
108
109impl Default for SectionResult {
110    /// The result before the first computation: no contours, zero length
111    /// and area, no fill.
112    fn default() -> Self {
113        SectionResult {
114            contours: Vec::new(),
115            length: 0.0,
116            area: 0.0,
117            fill: None,
118            fill_ok: true,
119        }
120    }
121}
122
123/// The closest pair between two meshes' contours in a section, from
124/// [`Scene::section_gap`](crate::Scene::section_gap).
125#[derive(Debug, Clone, Copy, PartialEq)]
126pub struct Gap {
127    /// The distance between `a` and `b` in the plane; 0 when the contours
128    /// cross or touch.
129    pub distance: f64,
130    /// The point on the first mesh's contours, in world coordinates.
131    pub a: [f64; 3],
132    /// The point on the second mesh's contours, in world coordinates.
133    pub b: [f64; 3],
134}
135
136#[cfg(test)]
137thread_local! {
138    /// Test-only: batches of entries computed in parallel on this thread.
139    static PARALLEL_SECTION_BATCHES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
140}
141
142/// Triangles, summed over the meshes of the section entries due in one
143/// update, from which the entries are cut in parallel, one entry per task,
144/// with the `parallel` feature.
145///
146/// Measured on the i5-13600K with one section over three spheres, dragging
147/// its plane (three entries due per plane): at 3 x 960 triangles parallel is
148/// 1.0 to 1.7 times slower, at 3 x 2,208 even to 1.4 times faster, at
149/// 3 x 3,968 1.45 times faster and at 3 x 36k 2.6 times.
150const PAR_MIN_SECTION_WORK: usize = 8 * 1024;
151
152fn oom() -> SceneError {
153    SceneError::Store(StoreError::OutOfMemory)
154}
155
156/// A `Vec` with room for `n` items, or `OutOfMemory`.
157fn try_vec<T>(n: usize) -> Result<Vec<T>> {
158    let mut v = Vec::new();
159    v.try_reserve_exact(n).map_err(|_| oom())?;
160    Ok(v)
161}
162
163/// What happened to one mesh of a section since its entry was last
164/// computed.
165#[derive(Debug, Clone)]
166pub(crate) struct SectionHook {
167    pub(crate) section: SectionId,
168    /// Merged changes; `None` means "everything".
169    pub(crate) changes: Option<ChangeSet>,
170    pub(crate) placement_changed: bool,
171    /// Whether the merged `changes` are already known to reach the plane
172    /// (see [`reaches_plane`]). Later edits skip the re-check, so a long
173    /// sculpt session without update does not cost quadratic time; merging
174    /// more changes keeps it set, and only a reset clears it.
175    pub(crate) reaches: bool,
176}
177
178impl SectionHook {
179    fn new(section: SectionId) -> SectionHook {
180        SectionHook {
181            section,
182            changes: Some(ChangeSet::default()),
183            placement_changed: false,
184            reaches: false,
185        }
186    }
187
188    fn is_clean(&self) -> bool {
189        !self.placement_changed && self.changes.as_ref().is_some_and(ChangeSet::is_empty)
190    }
191}
192
193/// A section's state for one selected mesh.
194struct SectionEntry {
195    mesh: MeshId,
196    result: SectionResult,
197    /// How many times the result has changed (see
198    /// [`Scene::section_revision`]).
199    revision: u64,
200    /// The plane or fill changed, or the mesh was just selected.
201    stale: bool,
202    computed_once: bool,
203    /// The vertices of the crossed edges, ascending without duplicates.
204    crossed_vertices: Vec<VertexId>,
205    /// The crossed triangles, ascending without duplicates.
206    crossed_triangles: Vec<TriangleId>,
207    /// The world plane of the last computation; `None` before the first.
208    plane: Option<Plane>,
209}
210
211impl SectionEntry {
212    fn new(mesh: MeshId) -> SectionEntry {
213        SectionEntry {
214            mesh,
215            result: SectionResult::default(),
216            revision: 0,
217            stale: true,
218            computed_once: false,
219            crossed_vertices: Vec::new(),
220            crossed_triangles: Vec::new(),
221            plane: None,
222        }
223    }
224}
225
226/// A registered section.
227pub(crate) struct Section {
228    id: SectionId,
229    desc: SectionDesc,
230    /// One per mesh of `desc.meshes`, in that order.
231    entries: Vec<SectionEntry>,
232}
233
234/// A freshly computed entry.
235struct Computed {
236    result: SectionResult,
237    crossed_vertices: Vec<VertexId>,
238    crossed_triangles: Vec<TriangleId>,
239}
240
241impl Entry {
242    fn section_hook(&self, id: SectionId) -> Option<&SectionHook> {
243        self.section_hooks.iter().find(|h| h.section == id)
244    }
245
246    fn reset_section_hook(&mut self, id: SectionId) {
247        if let Some(h) = self.section_hooks.iter_mut().find(|h| h.section == id) {
248            *h = SectionHook::new(id);
249        }
250    }
251
252    fn remove_section_hook(&mut self, id: SectionId) {
253        self.section_hooks.retain(|h| h.section != id);
254    }
255}
256
257/// `Rᵀ d`: a world direction in the mesh's local frame.
258fn rotate_back(rotation: &[[f64; 3]; 3], d: [f64; 3]) -> [f64; 3] {
259    let r = rotation;
260    std::array::from_fn(|i| r[0][i] * d[0] + r[1][i] * d[1] + r[2][i] * d[2])
261}
262
263/// `S (Rᵀ d)`: a world direction in the mesh's scaled local frame, before
264/// normalising. With a non-uniform scale this is not a rotation: axes mapped
265/// this way are re-orthonormalised below.
266fn scale_back(p: &Placement, d: [f64; 3]) -> [f64; 3] {
267    let t = rotate_back(&p.rotation, d);
268    std::array::from_fn(|i| p.scale[i] * t[i])
269}
270
271fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
272    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
273}
274
275fn unit(a: [f64; 3]) -> [f64; 3] {
276    let n = dot(a, a).sqrt();
277    a.map(|c| c / n)
278}
279
280/// The world plane in the mesh's stored local frame (origin excluded). The
281/// axes are the world `(u, v)` mapped through the placement's linear inverse
282/// (`S Rᵀ`, then re-orthonormalised: a placement's rotation is only
283/// orthonormal within `1e-6`, and a non-uniform scale skews the frame), so
284/// that the local plane's zero set is the preimage of the world's, with
285/// matching sides. Cut points map back to the world and their `(u, v)` come
286/// from the world plane, so lengths and areas follow the scale exactly.
287fn local_plane(p: &Placement, mesh_origin: [f64; 3], plane: &Plane) -> Result<Plane> {
288    let origin = world_to_local(p, mesh_origin, plane.origin());
289    let u = unit(scale_back(p, plane.u()));
290    let v = scale_back(p, plane.v());
291    let d = dot(v, u);
292    let v = unit(std::array::from_fn(|k| v[k] - d * u[k]));
293    Ok(Plane::with_axes(origin, u, v)?)
294}
295
296/// Cuts the entry's mesh with `desc`'s plane; its BVH must be current.
297fn compute(entry: &Entry, desc: &SectionDesc) -> Result<Computed> {
298    let plane = &desc.plane;
299    let (p, o) = (&entry.placement, entry.mesh.origin());
300    let mut cut = Slice::default();
301    fast_mesh_query::slice(
302        &entry.mesh,
303        &entry.bvh,
304        &local_plane(p, o, plane)?,
305        &mut cut,
306    )?;
307
308    let mut contours = try_vec(cut.contours.len())?;
309    let (mut length, mut area) = (0.0, 0.0);
310    let (mut nv, mut nt) = (0, 0);
311    for c in cut.contours {
312        let mut points = try_vec(c.points.len())?;
313        let mut uv = try_vec(c.points.len())?;
314        for &q in &c.points {
315            let w = local_to_world(p, o, q);
316            points.push(w);
317            uv.push(plane.to_uv(w));
318        }
319        let l = contour_length(&uv, c.closed);
320        let a = if c.closed { signed_area(&uv) } else { 0.0 };
321        length += l;
322        if c.closed && c.oriented {
323            area += a;
324        }
325        nv += 2 * c.edges.len();
326        nt += c.triangles.len();
327        contours.push(SectionContour {
328            points,
329            uv,
330            edges: c.edges,
331            triangles: c.triangles,
332            closed: c.closed,
333            oriented: c.oriented,
334            manifold: c.manifold,
335            length: l,
336            area: a,
337        });
338    }
339
340    let mut crossed_vertices = try_vec(nv)?;
341    let mut crossed_triangles = try_vec(nt)?;
342    for c in &contours {
343        crossed_vertices.extend(c.edges.iter().flat_map(|&(a, b)| [a, b]));
344        crossed_triangles.extend_from_slice(&c.triangles);
345    }
346    crossed_vertices.sort_unstable();
347    crossed_vertices.dedup();
348    crossed_triangles.sort_unstable();
349    crossed_triangles.dedup();
350
351    let (fill, fill_ok) = if desc.fill {
352        let mut closed = try_vec(contours.len())?;
353        closed.extend(
354            contours
355                .iter()
356                .filter(|c| c.closed)
357                .map(|c| (c.uv.as_slice(), c.oriented)),
358        );
359        let mut f = Fill::default();
360        let ok = fast_mesh_query::fill(&closed, &mut f)?;
361        (Some(f), ok)
362    } else {
363        (None, true)
364    };
365
366    Ok(Computed {
367        result: SectionResult {
368            contours,
369            length,
370            area,
371            fill,
372            fill_ok,
373        },
374        crossed_vertices,
375        crossed_triangles,
376    })
377}
378
379/// Whether the changes `c`, made since `se` was computed with the local
380/// plane `plane`, can alter its cut (see the module docs). `mesh` is the
381/// current mesh; its incidence index is built if needed, and failing that
382/// the answer is `true`.
383fn reaches_plane(mesh: &mut Mesh, plane: &Plane, c: &ChangeSet, se: &SectionEntry) -> bool {
384    let crossed_vertex = |v: &VertexId| se.crossed_vertices.binary_search(v).is_ok();
385    let crossed_triangle = |t: &TriangleId| se.crossed_triangles.binary_search(t).is_ok();
386    // 1 and 2: something the cut used moved or went.
387    if c.vertices_moved
388        .iter()
389        .chain(&c.vertices_removed)
390        .any(crossed_vertex)
391        || c.triangles_removed
392            .iter()
393            .chain(&c.triangles_changed)
394            .any(crossed_triangle)
395    {
396        return true;
397    }
398    // The side rule of the cut: `+` iff `s ≥ 0`, from the stored position.
399    let straddles = |mesh: &Mesh, t: TriangleId| {
400        mesh.triangle(t).is_some_and(|tri| {
401            let plus = tri.map(|v| {
402                let p = mesh.positions()[v as usize].map(f64::from);
403                plane.signed_distance(p) >= 0.0
404            });
405            plus[0] != plus[1] || plus[1] != plus[2]
406        })
407    };
408    // 4: a new or rewritten triangle crosses.
409    if c.triangles_added
410        .iter()
411        .chain(&c.triangles_changed)
412        .any(|&t| straddles(mesh, t))
413    {
414        return true;
415    }
416    // 3: a triangle around a moved or added vertex off the cut now crosses.
417    let mut off_cut = c
418        .vertices_moved
419        .iter()
420        .chain(&c.vertices_added)
421        .filter(|v| !crossed_vertex(v))
422        .peekable();
423    if off_cut.peek().is_none() {
424        return false;
425    }
426    if mesh.ensure_incidence().is_err() {
427        return true;
428    }
429    let mesh = &*mesh;
430    let Some(incidence) = mesh.incidence() else {
431        return true;
432    };
433    off_cut.any(|&v| incidence.triangles(v).iter().any(|&t| straddles(mesh, t)))
434}
435
436/// A polyline's `(u, v)` points and whether it is closed.
437type Polyline<'a> = (&'a [[f64; 2]], bool);
438
439/// The polylines of a result's contours, for [`contour_gap`].
440fn polylines(r: &SectionResult) -> Result<Vec<Polyline<'_>>> {
441    let mut out = try_vec(r.contours.len())?;
442    out.extend(r.contours.iter().map(|c| (c.uv.as_slice(), c.closed)));
443    Ok(out)
444}
445
446impl Scene {
447    /// Registers a plane section of the meshes `desc.meshes` and returns its
448    /// id. Sections are not in the history: undo does not move planes.
449    ///
450    /// Each selected mesh gets its own [`SectionResult`], which follows the
451    /// [`UpdateMode`] of that mesh: an Eager mesh's is computed right away
452    /// and after every edit, placement change, undo, redo or cancel of the
453    /// mesh, and after every plane or fill change; a Lazy one's by
454    /// [`Scene::flush`], [`Scene::update`] or [`Scene::update_sections`]; a
455    /// Manual one's by [`Scene::update`] or [`Scene::update_sections`]. A
456    /// failed Eager computation is swallowed and leaves the entry stale.
457    /// Until its first computation an entry reads as an empty result with
458    /// status [`SectionStatus::Stale`].
459    ///
460    /// Results that change are reported in the mesh's
461    /// [`MeshChanges::sections_changed`](crate::MeshChanges::sections_changed).
462    ///
463    /// # Errors
464    /// [`SceneError::UnknownMesh`] for a stale mesh id;
465    /// [`SceneError::InvalidInput`] for a mesh listed twice; a memory error.
466    /// Never [`SceneError::StaleBvh`]. The scene is unchanged on error.
467    pub fn add_section(&mut self, desc: SectionDesc) -> Result<SectionId> {
468        self.drop_field_unit();
469        self.check_selection(&desc.meshes)?;
470        let next = self
471            .next_section
472            .checked_add(1)
473            .ok_or(SceneError::InvalidInput("too many sections"))?;
474        let mut entries = try_vec(desc.meshes.len())?;
475        for &m in &desc.meshes {
476            self.entries
477                .get_mut(m)
478                .ok_or(SceneError::UnknownMesh)?
479                .section_hooks
480                .try_reserve(1)
481                .map_err(|_| oom())?;
482            entries.push(SectionEntry::new(m));
483        }
484        self.sections.try_reserve(1).map_err(|_| oom())?;
485        // Nothing can fail from here on.
486        let id = SectionId(self.next_section);
487        self.next_section = next;
488        for &m in &desc.meshes {
489            if let Some(e) = self.entries.get_mut(m) {
490                e.section_hooks.push(SectionHook::new(id));
491            }
492        }
493        self.sections.push(Section { id, desc, entries });
494        self.run_section_eager(self.sections.len() - 1);
495        Ok(id)
496    }
497
498    /// Removes a section. The removal is reported in the
499    /// [`MeshChanges::sections_changed`](crate::MeshChanges::sections_changed)
500    /// of every mesh it selected, and the id is rejected from then on.
501    ///
502    /// # Errors
503    /// [`SceneError::UnknownSection`] for an unknown or removed section.
504    pub fn remove_section(&mut self, id: SectionId) -> Result<()> {
505        self.drop_field_unit();
506        let i = self.section_index(id)?;
507        let s = self.sections.remove(i);
508        for se in &s.entries {
509            if let Some(e) = self.entries.get_mut(se.mesh) {
510                e.remove_section_hook(id);
511                e.report_section(id);
512            }
513        }
514        Ok(())
515    }
516
517    /// Moves a section's plane. Every entry of the section becomes stale
518    /// (Eager ones are recomputed right away); nothing else is touched.
519    /// Computing an Eager entry needs a current BVH, so an Eager mesh whose
520    /// earlier BVH update failed has that update retried here.
521    ///
522    /// # Errors
523    /// [`SceneError::UnknownSection`] for an unknown or removed section.
524    pub fn set_section_plane(&mut self, id: SectionId, plane: Plane) -> Result<()> {
525        self.drop_field_unit();
526        let i = self.section_index(id)?;
527        self.sections[i].desc.plane = plane;
528        self.restale_section(i);
529        Ok(())
530    }
531
532    /// Changes the meshes a section cuts. Meshes it keeps keep their entries;
533    /// added meshes start stale (Eager ones are computed right away); removed
534    /// meshes drop their entries, which is reported in their
535    /// [`MeshChanges::sections_changed`](crate::MeshChanges::sections_changed).
536    ///
537    /// # Errors
538    /// [`SceneError::UnknownSection`]; [`SceneError::UnknownMesh`] for a
539    /// stale mesh id; [`SceneError::InvalidInput`] for a mesh listed twice; a
540    /// memory error. Never [`SceneError::StaleBvh`]. The scene is unchanged
541    /// on error.
542    pub fn set_section_meshes(&mut self, id: SectionId, meshes: Vec<MeshId>) -> Result<()> {
543        self.drop_field_unit();
544        let i = self.section_index(id)?;
545        self.check_selection(&meshes)?;
546        let mut entries = try_vec(meshes.len())?;
547        let old = &self.sections[i].entries;
548        for &m in &meshes {
549            if !old.iter().any(|se| se.mesh == m) {
550                self.entries
551                    .get_mut(m)
552                    .ok_or(SceneError::UnknownMesh)?
553                    .section_hooks
554                    .try_reserve(1)
555                    .map_err(|_| oom())?;
556            }
557        }
558        // Nothing can fail from here on.
559        let mut old = std::mem::take(&mut self.sections[i].entries);
560        for &m in &meshes {
561            match old.iter().position(|se| se.mesh == m) {
562                Some(k) => entries.push(old.swap_remove(k)),
563                None => {
564                    if let Some(e) = self.entries.get_mut(m) {
565                        e.section_hooks.push(SectionHook::new(id));
566                    }
567                    entries.push(SectionEntry::new(m));
568                }
569            }
570        }
571        for se in &old {
572            if let Some(e) = self.entries.get_mut(se.mesh) {
573                e.remove_section_hook(id);
574                e.report_section(id);
575            }
576        }
577        let s = &mut self.sections[i];
578        s.entries = entries;
579        s.desc.meshes = meshes;
580        self.run_section_eager(i);
581        Ok(())
582    }
583
584    /// Turns a section's fill on or off. Every entry of the section becomes
585    /// stale (Eager ones are recomputed right away); nothing else is touched.
586    /// As with [`Scene::set_section_plane`], an Eager mesh whose earlier BVH
587    /// update failed has that update retried.
588    ///
589    /// # Errors
590    /// [`SceneError::UnknownSection`] for an unknown or removed section.
591    pub fn set_section_fill(&mut self, id: SectionId, fill: bool) -> Result<()> {
592        self.drop_field_unit();
593        let i = self.section_index(id)?;
594        self.sections[i].desc.fill = fill;
595        self.restale_section(i);
596        Ok(())
597    }
598
599    /// Computes every stale section entry, whatever the mode of its mesh,
600    /// bringing the BVHs it needs up to date under the query rules (Lazy
601    /// ones are updated). Renderers call this (or [`Scene::flush`]), then
602    /// read any number of results.
603    ///
604    /// # Errors
605    /// The first error, for example [`SceneError::StaleBvh`] for an entry
606    /// whose mesh has a stale Manual BVH: such an entry stays stale, and
607    /// makes every call return `StaleBvh` until that BVH is brought up to
608    /// date with [`Scene::update`]. Every entry is still attempted.
609    pub fn update_sections(&mut self) -> Result<()> {
610        self.drop_field_unit();
611        self.run_sections(|_| true, None)
612    }
613
614    /// The section's result for `mesh` as of its last computation (see
615    /// [`Scene::section_status`]); empty before the first one. Never
616    /// updates.
617    ///
618    /// # Errors
619    /// [`SceneError::UnknownSection`]; [`SceneError::UnknownMesh`] for a
620    /// stale mesh id; [`SceneError::InvalidInput`] for a live mesh the
621    /// section does not select.
622    pub fn section(&self, id: SectionId, mesh: MeshId) -> Result<&SectionResult> {
623        let (i, k) = self.section_entry(id, mesh)?;
624        Ok(&self.sections[i].entries[k].result)
625    }
626
627    /// The section's description as last set: its plane (the one
628    /// [`Scene::set_section_plane`] gave, which results not yet recomputed
629    /// may lag behind; see [`Scene::section_plane`]), meshes and fill.
630    ///
631    /// # Errors
632    /// [`SceneError::UnknownSection`] for an unknown or removed section.
633    pub fn section_desc(&self, id: SectionId) -> Result<&SectionDesc> {
634        let i = self.section_index(id)?;
635        Ok(&self.sections[i].desc)
636    }
637
638    /// Whether the section's result for `mesh` is current. Never updates.
639    ///
640    /// # Errors
641    /// As for [`Scene::section`].
642    pub fn section_status(&self, id: SectionId, mesh: MeshId) -> Result<SectionStatus> {
643        let (i, k) = self.section_entry(id, mesh)?;
644        Ok(if self.entry_current(i, k) {
645            SectionStatus::Current
646        } else {
647            SectionStatus::Stale
648        })
649    }
650
651    /// The world plane the section's result for `mesh` was computed with,
652    /// whose `(u, v)` frame its 2D points and fill are in; `None` before the
653    /// first computation. It differs from the section's plane while a plane
654    /// move is not yet computed. Never updates.
655    ///
656    /// # Errors
657    /// As for [`Scene::section`].
658    pub fn section_plane(&self, id: SectionId, mesh: MeshId) -> Result<Option<Plane>> {
659        let (i, k) = self.section_entry(id, mesh)?;
660        Ok(self.sections[i].entries[k].plane)
661    }
662
663    /// How many times the section's result for `mesh` has changed: 0 before
664    /// its first computation, then up by one per computation whose result
665    /// differs (a recompute with an identical result leaves it alone).
666    /// Bindings cache per-result data (like GPU buffers) against it instead
667    /// of draining a subscriber. Never updates.
668    ///
669    /// # Errors
670    /// As for [`Scene::section`].
671    pub fn section_revision(&self, id: SectionId, mesh: MeshId) -> Result<u64> {
672        let (i, k) = self.section_entry(id, mesh)?;
673        Ok(self.sections[i].entries[k].revision)
674    }
675
676    /// The closest pair between the contours of meshes `a` and `b` in the
677    /// section, measured in the plane, with its points in the world; `None`
678    /// when either has no contour segments. Never updates: it reads the last
679    /// computed results, so for a stale entry (see [`Scene::section_status`])
680    /// it describes the scene as of that computation. A gap is only defined
681    /// within one plane: when the two results were computed with different
682    /// planes (one was updated after a plane move, the other not yet), it is
683    /// `None`; call [`Scene::update_sections`] first.
684    ///
685    /// # Errors
686    /// As for [`Scene::section`], for either mesh;
687    /// [`SceneError::InvalidInput`] when `a` and `b` are the same mesh; a
688    /// memory error.
689    pub fn section_gap(&self, id: SectionId, a: MeshId, b: MeshId) -> Result<Option<Gap>> {
690        let (i, ka) = self.section_entry(id, a)?;
691        let (_, kb) = self.section_entry(id, b)?;
692        if a == b {
693            return Err(SceneError::InvalidInput(
694                "a section gap needs two different meshes",
695            ));
696        }
697        let s = &self.sections[i];
698        let (ea, eb) = (&s.entries[ka], &s.entries[kb]);
699        // Each side's points are in the frame of the plane it was cut with.
700        let (Some(plane_a), Some(plane_b)) = (ea.plane, eb.plane) else {
701            return Ok(None);
702        };
703        if plane_a != plane_b {
704            return Ok(None);
705        }
706        let pa = polylines(&ea.result)?;
707        let pb = polylines(&eb.result)?;
708        Ok(contour_gap(&pa, &pb)?.map(|g: Gap2| Gap {
709            distance: g.distance,
710            a: plane_a.from_uv(g.a),
711            b: plane_b.from_uv(g.b),
712        }))
713    }
714
715    /// The id of the section with this [`SectionId::raw`] value, or `None`
716    /// when there is none (never added, or removed).
717    pub fn section_id(&self, raw: u32) -> Option<SectionId> {
718        self.sections.iter().map(|s| s.id).find(|id| id.0 == raw)
719    }
720
721    fn section_index(&self, id: SectionId) -> Result<usize> {
722        self.sections
723            .iter()
724            .position(|s| s.id == id)
725            .ok_or(SceneError::UnknownSection)
726    }
727
728    /// The section's index and the index of `mesh`'s entry in it.
729    fn section_entry(&self, id: SectionId, mesh: MeshId) -> Result<(usize, usize)> {
730        let i = self.section_index(id)?;
731        self.entry(mesh)?;
732        let k = self.sections[i]
733            .entries
734            .iter()
735            .position(|se| se.mesh == mesh)
736            .ok_or(SceneError::InvalidInput(
737                "the section does not select this mesh",
738            ))?;
739        Ok((i, k))
740    }
741
742    /// Every mesh live and listed once.
743    fn check_selection(&self, meshes: &[MeshId]) -> Result<()> {
744        for (j, &m) in meshes.iter().enumerate() {
745            self.entry(m)?;
746            if meshes[..j].contains(&m) {
747                return Err(SceneError::InvalidInput(
748                    "a section lists a mesh more than once",
749                ));
750            }
751        }
752        Ok(())
753    }
754
755    /// Computed at least once, and nothing changed since.
756    fn entry_current(&self, i: usize, k: usize) -> bool {
757        let s = &self.sections[i];
758        let se = &s.entries[k];
759        se.computed_once
760            && !se.stale
761            && self
762                .entry(se.mesh)
763                .ok()
764                .and_then(|e| e.section_hook(s.id))
765                .is_some_and(SectionHook::is_clean)
766    }
767
768    /// Clears the pending changes of entry `k` of section `i` when they
769    /// cannot reach its plane (see the module docs), which leaves the entry
770    /// current with nothing recomputed or reported. Returns whether it did.
771    fn settle_entry(&mut self, i: usize, k: usize) -> bool {
772        let s = &self.sections[i];
773        let se = &s.entries[k];
774        if se.stale || !se.computed_once {
775            return false;
776        }
777        let Some(Entry {
778            mesh,
779            placement,
780            section_hooks,
781            ..
782        }) = self.entries.get_mut(se.mesh)
783        else {
784            return false;
785        };
786        let Some(h) = section_hooks.iter_mut().find(|h| h.section == s.id) else {
787            return false;
788        };
789        if h.placement_changed || h.reaches {
790            return false;
791        }
792        let Some(c) = &h.changes else {
793            return false;
794        };
795        if !c.is_empty() {
796            // The local plane the computation used: the plane, the placement
797            // and the mesh's origin are all unchanged since.
798            let Ok(plane) = local_plane(placement, mesh.origin(), &s.desc.plane) else {
799                return false;
800            };
801            #[cfg(test)]
802            {
803                self.section_reach_checks += 1;
804            }
805            if reaches_plane(mesh, &plane, c, se) {
806                h.reaches = true;
807                return false;
808            }
809        }
810        *h = SectionHook::new(s.id);
811        true
812    }
813
814    /// Stales every entry of section `i`, then computes the Eager ones.
815    fn restale_section(&mut self, i: usize) {
816        for se in &mut self.sections[i].entries {
817            se.stale = true;
818        }
819        self.run_section_eager(i);
820    }
821
822    /// Computes the stale entries of section `i` whose mesh is Eager,
823    /// swallowing failures.
824    fn run_section_eager(&mut self, i: usize) {
825        let mut due = Vec::new();
826        for k in 0..self.sections[i].entries.len() {
827            let eager = self
828                .entry(self.sections[i].entries[k].mesh)
829                .is_ok_and(|e| e.mode == UpdateMode::Eager);
830            if eager && !self.entry_current(i, k) {
831                if due.try_reserve(1).is_err() {
832                    return;
833                }
834                due.push((i, k));
835            }
836        }
837        let _ = self.compute_entries(&due);
838    }
839
840    /// Computes the stale entries whose mesh's mode passes `want` and (with
841    /// `touched`) is one of those meshes. Entries of those meshes whose
842    /// changes cannot reach the plane are settled first, whatever the mode.
843    /// Returns the first error; every entry is attempted.
844    pub(crate) fn run_sections(
845        &mut self,
846        want: impl Fn(UpdateMode) -> bool,
847        touched: Option<&[MeshId]>,
848    ) -> Result<()> {
849        let mut due = Vec::new();
850        for i in 0..self.sections.len() {
851            for k in 0..self.sections[i].entries.len() {
852                let mesh = self.sections[i].entries[k].mesh;
853                if touched.is_some_and(|t| !t.contains(&mesh)) {
854                    continue;
855                }
856                if self.entry_current(i, k) || self.settle_entry(i, k) {
857                    continue;
858                }
859                let Ok(e) = self.entry(mesh) else { continue };
860                if !want(e.mode) {
861                    continue;
862                }
863                due.try_reserve(1).map_err(|_| oom())?;
864                due.push((i, k));
865            }
866        }
867        self.compute_entries(&due)
868    }
869
870    /// Computes the Eager entries of `touched` (all with `None`). Failures
871    /// are swallowed: the entry stays stale (§6.3).
872    pub(crate) fn run_eager_sections(&mut self, touched: Option<&[MeshId]>) {
873        let _ = self.run_sections(|m| m == UpdateMode::Eager, touched);
874    }
875
876    /// Drops `mesh` from every section selecting it, reporting it on the
877    /// mesh (before its removal).
878    pub(crate) fn drop_sections_of(&mut self, mesh: MeshId) {
879        for s in &mut self.sections {
880            if let Some(k) = s.entries.iter().position(|se| se.mesh == mesh) {
881                s.entries.remove(k);
882                s.desc.meshes.retain(|&m| m != mesh);
883                if let Some(e) = self.entries.get_mut(mesh) {
884                    e.remove_section_hook(s.id);
885                    e.report_section(s.id);
886                }
887            }
888        }
889    }
890
891    /// Recomputes the entries `due` (`(section, entry)` index pairs, in
892    /// section order, then entry order) in full, each reported when its
893    /// result changed or was never computed, in three phases: each entry's
894    /// mesh BVH is brought up to date in turn under the query rules (a
895    /// non-Manual one is updated, a stale Manual one fails with
896    /// [`SceneError::StaleBvh`]); then every entry is cut without writing
897    /// (in parallel, one entry per task, when the work is large enough;
898    /// see [`PAR_MIN_SECTION_WORK`]); then each is written and reported in
899    /// order, as if the entries were recomputed one after the other.
900    ///
901    /// All or nothing per entry: on error the entry and its pending changes
902    /// stay as they were. Returns the first error in order; every entry is
903    /// attempted.
904    ///
905    /// Bringing a BVH up to date before the earlier entries are written
906    /// changes nothing: it writes no section state and reports nothing, and
907    /// an entry's cut reads only its mesh, BVH, placement and plane.
908    fn compute_entries(&mut self, due: &[(usize, usize)]) -> Result<()> {
909        let mut outcomes: Vec<Result<()>> = try_vec(due.len())?;
910        let mut jobs: Vec<(usize, usize, usize)> = try_vec(due.len())?;
911        for (n, &(i, k)) in due.iter().enumerate() {
912            let r = self.prepare_entry(i, k);
913            if r.is_ok() {
914                jobs.push((n, i, k));
915            }
916            outcomes.push(r);
917        }
918        match self.cut_entries(&jobs) {
919            Ok(cuts) => {
920                for ((n, i, k), c) in jobs.into_iter().zip(cuts) {
921                    outcomes[n] = c.map(|c| self.apply_entry(i, k, c));
922                }
923            }
924            Err(e) => {
925                for (n, _, _) in jobs {
926                    outcomes[n] = Err(e.clone());
927                }
928            }
929        }
930        outcomes.into_iter().find(Result::is_err).unwrap_or(Ok(()))
931    }
932
933    /// Brings the BVH of entry `k` of section `i` up to date under the query
934    /// rules (see [`Scene::compute_entries`]).
935    fn prepare_entry(&mut self, i: usize, k: usize) -> Result<()> {
936        let mesh = self.sections[i].entries[k].mesh;
937        let e = self.entries.get_mut(mesh).ok_or(SceneError::UnknownMesh)?;
938        if e.mode != UpdateMode::Manual {
939            e.update_bvh()?;
940        } else if !e.is_current() {
941            return Err(SceneError::StaleBvh(mesh));
942        }
943        Ok(())
944    }
945
946    /// Cuts the prepared entries `jobs` (`(_, section, entry)`) without
947    /// writing anything: one result per job, in job order. With the
948    /// `parallel` feature, when their meshes hold [`PAR_MIN_SECTION_WORK`]
949    /// triangles or more in all, the entries are cut in parallel, one job
950    /// per task.
951    ///
952    /// # Errors
953    /// `OutOfMemory` when the per-job results cannot be allocated.
954    fn cut_entries(&self, jobs: &[(usize, usize, usize)]) -> Result<Vec<Result<Computed>>> {
955        let mesh_of = |i: usize, k: usize| self.entry(self.sections[i].entries[k].mesh);
956        let work: usize = jobs
957            .iter()
958            .map(|&(_, i, k)| mesh_of(i, k).map_or(0, |e| e.mesh.triangle_count()))
959            .sum();
960        let parallel = work >= PAR_MIN_SECTION_WORK
961            && jobs.len() > 1
962            && par::enabled()
963            && !par::force_sequential();
964        #[cfg(test)]
965        if parallel {
966            PARALLEL_SECTION_BATCHES.with(|c| c.set(c.get() + 1));
967        }
968        let threshold = if parallel { 0 } else { usize::MAX };
969        // Chunks of one job; an entry's error is its own result.
970        par::map_chunks(jobs, 1, threshold, oom, |_, chunk| {
971            let (_, i, k) = chunk[0];
972            Ok(mesh_of(i, k).and_then(|e| compute(e, &self.sections[i].desc)))
973        })
974    }
975
976    /// Writes the fresh cut `c` into entry `k` of section `i`, and reports
977    /// it when the result changed or was never computed.
978    fn apply_entry(&mut self, i: usize, k: usize, c: Computed) {
979        #[cfg(test)]
980        {
981            self.section_recomputes += 1;
982        }
983        let s = &mut self.sections[i];
984        let id = s.id;
985        let se = &mut s.entries[k];
986        let changed = !se.computed_once || se.result != c.result;
987        if changed {
988            se.revision += 1;
989        }
990        se.result = c.result;
991        se.crossed_vertices = c.crossed_vertices;
992        se.crossed_triangles = c.crossed_triangles;
993        se.plane = Some(s.desc.plane);
994        se.stale = false;
995        se.computed_once = true;
996        if let Some(e) = self.entries.get_mut(se.mesh) {
997            e.reset_section_hook(id);
998            if changed {
999                e.report_section(id);
1000            }
1001        }
1002    }
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007    use std::cell::Cell;
1008    use std::f64::consts::PI;
1009
1010    use fast_mesh_store::Mesh;
1011
1012    use super::*;
1013    use crate::{MeshChanges, MeshOptions, Placement, SceneOptions, SubscriberId};
1014
1015    /// A closed UV sphere of radius `r` around `centre`, outward wound: one
1016    /// vertex at each pole and `stacks - 1` rings of `slices` vertices.
1017    /// Built from `f64` positions, so its origin is the centre of its box.
1018    fn sphere(stacks: u32, slices: u32, r: f64, centre: [f64; 3]) -> Mesh {
1019        let mut pos = vec![centre[0], centre[1], centre[2] + r];
1020        for i in 1..stacks {
1021            let theta = PI * f64::from(i) / f64::from(stacks);
1022            for j in 0..slices {
1023                let phi = 2.0 * PI * f64::from(j) / f64::from(slices);
1024                let (rs, rc) = (r * theta.sin(), r * theta.cos());
1025                pos.extend([
1026                    centre[0] + rs * phi.cos(),
1027                    centre[1] + rs * phi.sin(),
1028                    centre[2] + rc,
1029                ]);
1030            }
1031        }
1032        pos.extend([centre[0], centre[1], centre[2] - r]);
1033        let south = 1 + (stacks - 1) * slices;
1034        let ring = |i: u32, j: u32| 1 + (i - 1) * slices + j % slices;
1035        let mut idx = Vec::new();
1036        for j in 0..slices {
1037            idx.extend([0, ring(1, j), ring(1, j + 1)]);
1038            for i in 1..stacks - 1 {
1039                let (a, b, c, d) = (
1040                    ring(i, j),
1041                    ring(i + 1, j),
1042                    ring(i + 1, j + 1),
1043                    ring(i, j + 1),
1044                );
1045                idx.extend([a, b, c, a, c, d]);
1046            }
1047            idx.extend([ring(stacks - 1, j), south, ring(stacks - 1, j + 1)]);
1048        }
1049        Mesh::from_buffers_f64(&pos, &idx).unwrap()
1050    }
1051
1052    fn unit_sphere() -> Mesh {
1053        sphere(32, 64, 1.0, [0.0; 3])
1054    }
1055
1056    fn with_mode(mode: UpdateMode) -> MeshOptions {
1057        MeshOptions {
1058            mode,
1059            ..MeshOptions::default()
1060        }
1061    }
1062
1063    fn at(x: f64, y: f64, z: f64) -> Placement {
1064        Placement {
1065            translation: [x, y, z],
1066            ..Placement::IDENTITY
1067        }
1068    }
1069
1070    fn z_plane(h: f64) -> Plane {
1071        Plane::new([0.0, 0.0, h], [0.0, 0.0, 1.0]).unwrap()
1072    }
1073
1074    #[test]
1075    fn scaled_section_follows_the_scale() {
1076        // The unit sphere scaled by [2, 1, 1] and moved to [0, 3, 0]: the
1077        // world shape is x in [-2, 2], y in [2, 4]. The plane x = 1 cuts a
1078        // circle of radius sqrt(1 - 1/4) around [1, 3, 0].
1079        let mut scene = Scene::new(SceneOptions::default());
1080        let m = scene
1081            .add_mesh(
1082                unit_sphere(),
1083                MeshOptions {
1084                    placement: Placement {
1085                        translation: [0.0, 3.0, 0.0],
1086                        scale: [2.0, 1.0, 1.0],
1087                        ..Placement::IDENTITY
1088                    },
1089                    ..MeshOptions::default()
1090                },
1091            )
1092            .unwrap();
1093        scene.take_changes();
1094        let plane = Plane::new([1.0, 3.0, 0.0], [1.0, 0.0, 0.0]).unwrap();
1095        let id = scene.add_section(desc(plane, &[m], false)).unwrap();
1096        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Current));
1097        let r = scene.section(id, m).unwrap().clone();
1098        assert_eq!(r.contours.len(), 1);
1099        let c = &r.contours[0];
1100        assert!(c.closed && c.oriented && c.manifold);
1101        assert!(
1102            c.points.iter().all(|p| (p[0] - 1.0).abs() < 1e-6),
1103            "{:?}",
1104            &c.points[..4.min(c.points.len())]
1105        );
1106        let want_len = 2.0 * PI * (0.75f64).sqrt();
1107        assert!(
1108            close(r.length, want_len, 2e-2),
1109            "{} vs {want_len}",
1110            r.length
1111        );
1112        assert!(
1113            close(r.area, PI * 0.75, 2e-2),
1114            "{} vs {}",
1115            r.area,
1116            PI * 0.75
1117        );
1118        assert_fresh(&mut scene, id, m, "scaled");
1119        // Moving the mesh restales the entry until it is recomputed.
1120        scene
1121            .set_placement(
1122                m,
1123                Placement {
1124                    translation: [0.0, 3.0, 2.0],
1125                    scale: [2.0, 1.0, 1.0],
1126                    ..Placement::IDENTITY
1127                },
1128            )
1129            .unwrap();
1130        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Current));
1131        let r = scene.section(id, m).unwrap();
1132        assert!(close(r.length, want_len, 2e-2), "{}", r.length);
1133        assert!(
1134            r.contours[0]
1135                .points
1136                .iter()
1137                .all(|p| (p[0] - 1.0).abs() < 1e-6)
1138        );
1139    }
1140
1141    fn desc(plane: Plane, meshes: &[MeshId], fill: bool) -> SectionDesc {
1142        SectionDesc {
1143            plane,
1144            meshes: meshes.to_vec(),
1145            fill,
1146        }
1147    }
1148
1149    fn close(value: f64, expected: f64, rel: f64) -> bool {
1150        (value - expected).abs() <= rel * expected.abs()
1151    }
1152
1153    /// The circumference of the cut of a unit sphere at height `h`.
1154    fn circle(h: f64) -> f64 {
1155        2.0 * PI * (1.0 - h * h).sqrt()
1156    }
1157
1158    /// A scene with one unit sphere in `mode`, its add already taken.
1159    fn one_sphere(mode: UpdateMode) -> (Scene, MeshId) {
1160        let mut scene = Scene::new(SceneOptions::default());
1161        let m = scene.add_mesh(unit_sphere(), with_mode(mode)).unwrap();
1162        scene.take_changes();
1163        (scene, m)
1164    }
1165
1166    /// The sections reported on `mesh` in `ch`.
1167    fn of(ch: &[MeshChanges], mesh: MeshId) -> Vec<SectionId> {
1168        ch.iter()
1169            .find(|c| c.id == mesh)
1170            .map_or(Vec::new(), |c| c.sections_changed.clone())
1171    }
1172
1173    /// The section's result for `mesh` is current and equals one computed
1174    /// from scratch by a new section with the same plane and fill.
1175    fn assert_fresh(scene: &mut Scene, id: SectionId, mesh: MeshId, at: &str) {
1176        scene.update_sections().unwrap();
1177        assert_eq!(
1178            scene.section_status(id, mesh),
1179            Ok(SectionStatus::Current),
1180            "{at}"
1181        );
1182        let s = &scene.sections[scene.section_index(id).unwrap()];
1183        let d = desc(s.desc.plane, &[mesh], s.desc.fill);
1184        let fresh = scene.add_section(d).unwrap();
1185        scene.update_sections().unwrap();
1186        let want = scene.section(fresh, mesh).unwrap().clone();
1187        assert_eq!(scene.section(id, mesh).unwrap(), &want, "{at}");
1188        scene.remove_section(fresh).unwrap();
1189    }
1190
1191    fn fill_area(f: &Fill) -> f64 {
1192        f.triangles
1193            .iter()
1194            .map(|t| {
1195                let [a, b, c] = t.map(|i| f.points[i as usize]);
1196                0.5 * ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]))
1197            })
1198            .sum()
1199    }
1200
1201    /// The rotation of the quaternion `q` (normalised first).
1202    fn rotation(q: [f64; 4]) -> [[f64; 3]; 3] {
1203        let n = q.iter().map(|c| c * c).sum::<f64>().sqrt();
1204        let [w, x, y, z] = q.map(|c| c / n);
1205        [
1206            [
1207                1.0 - 2.0 * (y * y + z * z),
1208                2.0 * (x * y - w * z),
1209                2.0 * (x * z + w * y),
1210            ],
1211            [
1212                2.0 * (x * y + w * z),
1213                1.0 - 2.0 * (x * x + z * z),
1214                2.0 * (y * z - w * x),
1215            ],
1216            [
1217                2.0 * (x * z - w * y),
1218                2.0 * (y * z + w * x),
1219                1.0 - 2.0 * (x * x + y * y),
1220            ],
1221        ]
1222    }
1223
1224    fn remove_triangles_using(scene: &mut Scene, m: MeshId, v: VertexId) -> Vec<TriangleId> {
1225        let tris: Vec<TriangleId> = scene
1226            .mesh(m)
1227            .unwrap()
1228            .triangles()
1229            .iter()
1230            .enumerate()
1231            .filter(|(t, tri)| {
1232                scene.mesh(m).unwrap().is_triangle_alive(*t as TriangleId) && tri.contains(&v)
1233            })
1234            .map(|(t, _)| t as TriangleId)
1235            .collect();
1236        scene.edit(m, |tx| tx.remove_triangles(&tris)).unwrap();
1237        tris
1238    }
1239
1240    #[test]
1241    fn add_section_eager_computes_immediately() {
1242        let (mut scene, m) = one_sphere(UpdateMode::Eager);
1243        let id = scene.add_section(desc(z_plane(0.3), &[m], false)).unwrap();
1244        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Current));
1245        let r = scene.section(id, m).unwrap().clone();
1246        assert_eq!(r.contours.len(), 1);
1247        let c = &r.contours[0];
1248        assert!(c.closed && c.oriented && c.manifold);
1249        assert!(close(r.length, 2.0 * PI * 0.954, 1e-2), "{}", r.length);
1250        assert!(close(r.length, circle(0.3), 1e-2));
1251        assert_eq!(r.length, c.length);
1252        assert!(r.area > 0.0 && r.area == c.area);
1253        assert!(close(r.area, PI * (1.0 - 0.09), 1e-2), "{}", r.area);
1254        assert!(r.fill.is_none() && r.fill_ok);
1255        assert_eq!(c.points.len(), c.uv.len());
1256        assert_eq!(c.points.len(), c.edges.len());
1257        assert_eq!(c.triangles.len(), c.points.len());
1258        assert!(c.points.iter().all(|p| (p[2] - 0.3).abs() < 1e-6));
1259        let ch = scene.take_changes();
1260        assert_eq!(ch.len(), 1);
1261        assert_eq!(ch[0].sections_changed, [id]);
1262        assert_eq!(scene.section_recomputes, 1);
1263        // The crossed sets cover the contour.
1264        let se = &scene.sections[0].entries[0];
1265        assert_eq!(se.crossed_triangles.len(), c.triangles.len());
1266        assert!(se.crossed_vertices.windows(2).all(|w| w[0] < w[1]));
1267        assert!(
1268            c.edges
1269                .iter()
1270                .all(|&(a, b)| se.crossed_vertices.binary_search(&a).is_ok()
1271                    && se.crossed_vertices.binary_search(&b).is_ok())
1272        );
1273    }
1274
1275    #[test]
1276    fn lazy_waits_for_update_sections() {
1277        let (mut scene, m) = one_sphere(UpdateMode::Lazy);
1278        let id = scene.add_section(desc(z_plane(0.3), &[m], true)).unwrap();
1279        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Stale));
1280        assert_eq!(scene.section(id, m), Ok(&SectionResult::default()));
1281        assert!(scene.take_changes().is_empty());
1282        scene.update_sections().unwrap();
1283        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Current));
1284        let r = scene.section(id, m).unwrap();
1285        assert_eq!(r.contours.len(), 1);
1286        assert!(r.fill.is_some() && r.fill_ok);
1287        assert_eq!(scene.take_changes()[0].sections_changed, [id]);
1288        // Nothing stale: nothing recomputed or reported.
1289        scene.update_sections().unwrap();
1290        scene.flush().unwrap();
1291        assert_eq!(scene.section_recomputes, 1);
1292        assert!(scene.take_changes().is_empty());
1293        // An edit stales it until a flush.
1294        scene
1295            .edit(m, |tx| tx.set_positions(&[40], &[[0.0, 0.0, 0.0]]))
1296            .unwrap();
1297        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Stale));
1298        scene.flush().unwrap();
1299        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Current));
1300        assert_fresh(&mut scene, id, m, "after flush");
1301    }
1302
1303    #[test]
1304    fn plane_move_restales_and_recomputes_eager() {
1305        let (mut scene, m) = one_sphere(UpdateMode::Eager);
1306        let id = scene.add_section(desc(z_plane(0.3), &[m], false)).unwrap();
1307        scene.set_section_plane(id, z_plane(-0.5)).unwrap();
1308        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Current));
1309        let r = scene.section(id, m).unwrap();
1310        assert!(close(r.length, circle(0.5), 1e-2), "{}", r.length);
1311        assert!(
1312            r.contours[0]
1313                .points
1314                .iter()
1315                .all(|p| (p[2] + 0.5).abs() < 1e-6)
1316        );
1317        assert_eq!(scene.section_recomputes, 2);
1318        assert_fresh(&mut scene, id, m, "eager");
1319
1320        // A Lazy mesh keeps the old result, Stale, until updated.
1321        let (mut scene, m) = one_sphere(UpdateMode::Lazy);
1322        let id = scene.add_section(desc(z_plane(0.3), &[m], false)).unwrap();
1323        scene.update_sections().unwrap();
1324        let old = scene.section(id, m).unwrap().clone();
1325        scene.set_section_plane(id, z_plane(-0.5)).unwrap();
1326        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Stale));
1327        assert_eq!(scene.section(id, m), Ok(&old));
1328        // The description has the new plane while the result lags.
1329        assert_eq!(scene.section_desc(id).unwrap().plane, z_plane(-0.5));
1330        assert_eq!(scene.section_plane(id, m), Ok(Some(z_plane(0.3))));
1331        scene.update_sections().unwrap();
1332        let r = scene.section(id, m).unwrap();
1333        assert!(close(r.length, circle(0.5), 1e-2));
1334        // A fill change stales it too.
1335        scene.set_section_fill(id, true).unwrap();
1336        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Stale));
1337        assert!(scene.section(id, m).unwrap().fill.is_none());
1338        scene.update_sections().unwrap();
1339        assert!(scene.section(id, m).unwrap().fill.is_some());
1340    }
1341
1342    #[test]
1343    fn world_points_match_local_slice_with_origin() {
1344        let mut state = 0x2545_f491_4f6c_dd1du64;
1345        let mut next = || {
1346            state = state
1347                .wrapping_mul(6364136223846793005)
1348                .wrapping_add(1442695040888963407);
1349            (state >> 11) as f64 / (1u64 << 53) as f64 - 0.5
1350        };
1351        let centre = [1000.0, -1000.0, 500.0];
1352        for _ in 0..8 {
1353            let mut scene = Scene::new(SceneOptions::default());
1354            let placement = Placement {
1355                rotation: rotation([next(), next(), next(), next()]),
1356                translation: [next() * 50.0, next() * 50.0, next() * 50.0],
1357                ..Placement::IDENTITY
1358            };
1359            let opts = MeshOptions {
1360                placement,
1361                ..MeshOptions::default()
1362            };
1363            let m = scene.add_mesh(sphere(24, 48, 1.0, centre), opts).unwrap();
1364            let o = scene.mesh(m).unwrap().origin();
1365            assert!((0..3).all(|k| (o[k] - centre[k]).abs() < 1e-9), "{o:?}");
1366            // A plane through the sphere, in the world.
1367            let c = placement.apply(centre);
1368            let n = [next(), next(), next()];
1369            let off = next() * 1.2;
1370            let len = n.iter().map(|x| x * x).sum::<f64>().sqrt();
1371            let origin: [f64; 3] = std::array::from_fn(|k| c[k] + off * n[k] / len);
1372            let plane = Plane::new(origin, n).unwrap();
1373            let id = scene.add_section(desc(plane, &[m], false)).unwrap();
1374            let r = scene.section(id, m).unwrap().clone();
1375            assert!(!r.contours.is_empty());
1376
1377            // The same cut made directly in local space.
1378            let r_t = |d: [f64; 3]| -> [f64; 3] {
1379                let r = &placement.rotation;
1380                std::array::from_fn(|i| r[0][i] * d[0] + r[1][i] * d[1] + r[2][i] * d[2])
1381            };
1382            let local = Plane::with_axes(
1383                scene.world_to_local(m, plane.origin()).unwrap(),
1384                r_t(plane.u()),
1385                r_t(plane.v()),
1386            )
1387            .unwrap();
1388            let mut cut = Slice::default();
1389            fast_mesh_query::slice(
1390                scene.mesh(m).unwrap(),
1391                scene.bvh(m).unwrap(),
1392                &local,
1393                &mut cut,
1394            )
1395            .unwrap();
1396            assert_eq!(cut.contours.len(), r.contours.len());
1397            let tol = 1e-9 * 1000.0;
1398            for (want, got) in cut.contours.iter().zip(&r.contours) {
1399                assert_eq!(want.edges, got.edges);
1400                assert_eq!(want.triangles, got.triangles);
1401                assert_eq!(
1402                    (want.closed, want.oriented, want.manifold),
1403                    (got.closed, got.oriented, got.manifold)
1404                );
1405                for ((&l, &w), &uv) in want.points.iter().zip(&got.points).zip(&got.uv) {
1406                    let p = scene.local_to_world(m, l).unwrap();
1407                    assert!((0..3).all(|k| (p[k] - w[k]).abs() <= tol), "{p:?} {w:?}");
1408                    let q = plane.to_uv(w);
1409                    assert!((0..2).all(|k| (q[k] - uv[k]).abs() <= tol), "{q:?} {uv:?}");
1410                    assert!(plane.signed_distance(w).abs() < 1e-6);
1411                }
1412            }
1413            // Outward winding stays counter-clockwise in the world frame.
1414            assert!(r.area > 0.0);
1415        }
1416    }
1417
1418    #[test]
1419    fn plane_missing_mesh_gives_empty_current_result() {
1420        let (mut scene, m) = one_sphere(UpdateMode::Eager);
1421        let id = scene.add_section(desc(z_plane(5.0), &[m], true)).unwrap();
1422        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Current));
1423        let r = scene.section(id, m).unwrap();
1424        assert!(r.contours.is_empty());
1425        assert_eq!((r.length, r.area), (0.0, 0.0));
1426        assert_eq!(r.fill, Some(Fill::default()));
1427        assert!(r.fill_ok);
1428        // The first computation is reported, even when empty.
1429        assert_eq!(scene.take_changes()[0].sections_changed, [id]);
1430        // Still missing: recomputed to the same result, not reported.
1431        scene.set_section_plane(id, z_plane(6.0)).unwrap();
1432        assert_eq!(scene.section_recomputes, 2);
1433        assert!(scene.take_changes().is_empty());
1434    }
1435
1436    #[test]
1437    fn edit_undo_redo_keep_result_equal_to_from_scratch() {
1438        type Op = Box<dyn Fn(&mut Scene, MeshId)>;
1439        // A vertex of the ring just above the plane (ring 16 is the equator).
1440        let v: VertexId = 1 + 15 * 64 + 3;
1441        let ops: Vec<(&str, Op)> = vec![
1442            (
1443                "move across",
1444                Box::new(move |s, m| {
1445                    s.edit(m, |tx| tx.set_positions(&[v], &[[0.9, 0.2, -0.2]]))
1446                        .unwrap();
1447                }),
1448            ),
1449            (
1450                "remove triangles",
1451                Box::new(move |s, m| {
1452                    remove_triangles_using(s, m, v + 1);
1453                }),
1454            ),
1455            ("undo", Box::new(|s, _| assert!(s.undo().unwrap()))),
1456            ("undo", Box::new(|s, _| assert!(s.undo().unwrap()))),
1457            ("redo", Box::new(|s, _| assert!(s.redo().unwrap()))),
1458            (
1459                "placement",
1460                Box::new(|s, m| s.set_placement(m, at(0.1, 0.0, 0.02)).unwrap()),
1461            ),
1462            (
1463                "cancelled drag",
1464                Box::new(move |s, m| {
1465                    s.begin_step();
1466                    s.set_placement(m, at(0.0, 0.0, 0.6)).unwrap();
1467                    s.edit(m, |tx| tx.set_positions(&[v], &[[0.0, 0.0, 0.0]]))
1468                        .unwrap();
1469                    s.cancel_step().unwrap();
1470                }),
1471            ),
1472            (
1473                "undo placement",
1474                Box::new(|s, _| assert!(s.undo().unwrap())),
1475            ),
1476            (
1477                "redo placement",
1478                Box::new(|s, _| assert!(s.redo().unwrap())),
1479            ),
1480        ];
1481        for mode in [UpdateMode::Eager, UpdateMode::Lazy, UpdateMode::Manual] {
1482            let (mut scene, m) = one_sphere(mode);
1483            let id = scene.add_section(desc(z_plane(0.05), &[m], true)).unwrap();
1484            assert_fresh(&mut scene, id, m, "start");
1485            for (name, op) in &ops {
1486                let before = scene.section(id, m).unwrap().clone();
1487                op(&mut scene, m);
1488                let expect = if mode == UpdateMode::Eager {
1489                    SectionStatus::Current
1490                } else {
1491                    SectionStatus::Stale
1492                };
1493                assert_eq!(scene.section_status(id, m), Ok(expect), "{mode:?} {name}");
1494                if mode != UpdateMode::Eager {
1495                    assert_eq!(scene.section(id, m), Ok(&before), "{mode:?} {name}");
1496                }
1497                if mode == UpdateMode::Manual {
1498                    scene.update(m).unwrap();
1499                }
1500                assert_fresh(&mut scene, id, m, &format!("{mode:?} {name}"));
1501            }
1502        }
1503    }
1504
1505    #[test]
1506    fn placement_change_recomputes() {
1507        let (mut scene, m) = one_sphere(UpdateMode::Eager);
1508        let id = scene.add_section(desc(z_plane(0.3), &[m], false)).unwrap();
1509        scene.take_changes();
1510        scene.set_placement(m, at(0.0, 0.0, 0.2)).unwrap();
1511        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Current));
1512        let r = scene.section(id, m).unwrap();
1513        assert!(close(r.length, circle(0.1), 1e-2), "{}", r.length);
1514        assert!(
1515            r.contours[0]
1516                .points
1517                .iter()
1518                .all(|p| (p[2] - 0.3).abs() < 1e-6)
1519        );
1520        let ch = scene.take_changes();
1521        assert!(ch[0].placement_changed);
1522        assert_eq!(ch[0].sections_changed, [id]);
1523        assert_fresh(&mut scene, id, m, "moved");
1524    }
1525
1526    #[test]
1527    fn fill_on_gives_area_matching_contours() {
1528        let (mut scene, m) = one_sphere(UpdateMode::Eager);
1529        let id = scene.add_section(desc(z_plane(0.3), &[m], true)).unwrap();
1530        let r = scene.section(id, m).unwrap();
1531        assert!(r.fill_ok);
1532        let f = r.fill.as_ref().unwrap();
1533        assert!(!f.triangles.is_empty());
1534        let a = fill_area(f);
1535        assert!(close(a, r.area, 1e-6), "{a} {}", r.area);
1536        assert!(close(r.area, PI * (1.0 - 0.09), 1e-2));
1537        // Turning fill off drops it.
1538        scene.set_section_fill(id, false).unwrap();
1539        let r = scene.section(id, m).unwrap();
1540        assert!(r.fill.is_none() && r.fill_ok);
1541    }
1542
1543    #[test]
1544    fn gap_between_two_spheres() {
1545        let mut scene = Scene::new(SceneOptions::default());
1546        let a = scene
1547            .add_mesh(unit_sphere(), MeshOptions::default())
1548            .unwrap();
1549        let opts = MeshOptions {
1550            placement: at(3.0, 0.0, 0.0),
1551            ..MeshOptions::default()
1552        };
1553        let b = scene.add_mesh(unit_sphere(), opts).unwrap();
1554        let id = scene
1555            .add_section(desc(z_plane(0.0), &[a, b], false))
1556            .unwrap();
1557        let g = scene.section_gap(id, a, b).unwrap().unwrap();
1558        assert!((g.distance - 1.0).abs() < 1e-3, "{}", g.distance);
1559        assert!((g.a[0] - 1.0).abs() < 1e-3 && (g.b[0] - 2.0).abs() < 1e-3);
1560        for p in [g.a, g.b] {
1561            assert!(p[1].abs() < 1e-3 && p[2].abs() < 1e-9, "{p:?}");
1562        }
1563        let d = (0..3)
1564            .map(|k| (g.a[k] - g.b[k]).powi(2))
1565            .sum::<f64>()
1566            .sqrt();
1567        assert!((d - g.distance).abs() < 1e-9);
1568        // Symmetric, and None when one side has no contours.
1569        let h = scene.section_gap(id, b, a).unwrap().unwrap();
1570        assert!((h.distance - g.distance).abs() < 1e-12);
1571        scene.set_placement(b, at(3.0, 0.0, 5.0)).unwrap();
1572        assert_eq!(scene.section_gap(id, a, b), Ok(None));
1573    }
1574
1575    #[test]
1576    fn error_paths() {
1577        let mut scene = Scene::new(SceneOptions::default());
1578        let a = scene
1579            .add_mesh(unit_sphere(), MeshOptions::default())
1580            .unwrap();
1581        let b = scene
1582            .add_mesh(unit_sphere(), MeshOptions::default())
1583            .unwrap();
1584        let gone = scene
1585            .add_mesh(unit_sphere(), MeshOptions::default())
1586            .unwrap();
1587        scene.remove_mesh(gone).unwrap();
1588        let p = z_plane(0.0);
1589        let invalid = |r: &Result<SectionId>| matches!(r, Err(SceneError::InvalidInput(_)));
1590        assert_eq!(
1591            scene.add_section(desc(p, &[a, gone], false)),
1592            Err(SceneError::UnknownMesh)
1593        );
1594        assert!(invalid(&scene.add_section(desc(p, &[a, b, a], false))));
1595        assert!(scene.sections.is_empty());
1596        let no_hooks = |s: &Scene| {
1597            s.ids()
1598                .all(|m| s.entry(m).unwrap().section_hooks.is_empty())
1599        };
1600        assert!(no_hooks(&scene));
1601
1602        let id = scene.add_section(desc(p, &[a], false)).unwrap();
1603        assert_eq!(
1604            scene.set_section_meshes(id, vec![a, gone]),
1605            Err(SceneError::UnknownMesh)
1606        );
1607        assert!(matches!(
1608            scene.set_section_meshes(id, vec![b, b]),
1609            Err(SceneError::InvalidInput(_))
1610        ));
1611        assert_eq!(scene.sections[0].desc.meshes, [a]);
1612        assert!(scene.entry(b).unwrap().section_hooks.is_empty());
1613        assert!(matches!(
1614            scene.section(id, b),
1615            Err(SceneError::InvalidInput(_))
1616        ));
1617        assert!(matches!(
1618            scene.section_status(id, b),
1619            Err(SceneError::InvalidInput(_))
1620        ));
1621        assert!(matches!(
1622            scene.section_gap(id, a, b),
1623            Err(SceneError::InvalidInput(_))
1624        ));
1625        assert_eq!(scene.section(id, gone), Err(SceneError::UnknownMesh));
1626        assert_eq!(scene.section_gap(id, gone, a), Err(SceneError::UnknownMesh));
1627
1628        assert_eq!(scene.section_id(id.raw()), Some(id));
1629        assert_eq!(scene.section_id(id.raw() + 1), None);
1630        scene.remove_section(id).unwrap();
1631        assert_eq!(scene.section_id(id.raw()), None);
1632        assert!(no_hooks(&scene));
1633        let u = SceneError::UnknownSection;
1634        assert_eq!(scene.remove_section(id), Err(u.clone()));
1635        assert_eq!(scene.set_section_plane(id, p), Err(u.clone()));
1636        assert_eq!(scene.set_section_meshes(id, vec![a]), Err(u.clone()));
1637        assert_eq!(scene.set_section_fill(id, true), Err(u.clone()));
1638        assert_eq!(scene.section(id, a), Err(u.clone()));
1639        assert_eq!(scene.section_status(id, a), Err(u.clone()));
1640        assert_eq!(scene.section_gap(id, a, a), Err(u.clone()));
1641        assert_eq!(u.to_string(), "unknown or removed section id");
1642        // Ids are not reused.
1643        let again = scene.add_section(desc(p, &[a], false)).unwrap();
1644        assert_ne!(again, id);
1645        assert_eq!(scene.section(id, a), Err(u));
1646    }
1647
1648    #[test]
1649    fn remove_section_and_mesh_report_once() {
1650        let mut scene = Scene::new(SceneOptions::default());
1651        let a = scene
1652            .add_mesh(unit_sphere(), MeshOptions::default())
1653            .unwrap();
1654        let b = scene
1655            .add_mesh(unit_sphere(), with_mode(UpdateMode::Lazy))
1656            .unwrap();
1657        let s2 = scene.subscribe();
1658        let id = scene
1659            .add_section(desc(z_plane(0.2), &[a, b], false))
1660            .unwrap();
1661        scene.update_sections().unwrap();
1662        scene.take_changes();
1663        scene.take_changes_for(s2).unwrap();
1664        scene.remove_section(id).unwrap();
1665        for s in [SubscriberId::DEFAULT, s2] {
1666            let ch = scene.take_changes_for(s).unwrap();
1667            assert_eq!(ch.len(), 2);
1668            for c in &ch {
1669                assert_eq!(c.sections_changed, [id], "{:?}", c.id);
1670                assert!(c.changes.as_ref().unwrap().is_empty());
1671            }
1672        }
1673
1674        let id2 = scene
1675            .add_section(desc(z_plane(0.2), &[a, b], false))
1676            .unwrap();
1677        scene.take_changes();
1678        scene.take_changes_for(s2).unwrap();
1679        scene.remove_mesh(b).unwrap();
1680        assert_eq!(scene.sections[0].desc.meshes, [a]);
1681        assert_eq!(scene.section(id2, b), Err(SceneError::UnknownMesh));
1682        assert_eq!(scene.section_status(id2, b), Err(SceneError::UnknownMesh));
1683        assert_eq!(scene.section_status(id2, a), Ok(SectionStatus::Current));
1684        for s in [SubscriberId::DEFAULT, s2] {
1685            let ch = scene.take_changes_for(s).unwrap();
1686            assert_eq!(ch.len(), 1);
1687            assert!(ch[0].removed && ch[0].id == b);
1688            assert_eq!(ch[0].sections_changed, [id2]);
1689        }
1690        scene.update_sections().unwrap();
1691        scene.remove_section(id2).unwrap();
1692        assert_eq!(of(&scene.take_changes_for(s2).unwrap(), a), [id2]);
1693        assert_eq!(scene.section(id2, a), Err(SceneError::UnknownSection));
1694        assert_eq!(scene.section_status(id, a), Err(SceneError::UnknownSection));
1695    }
1696
1697    #[test]
1698    fn selection_change_keeps_and_drops_entries() {
1699        let mut scene = Scene::new(SceneOptions::default());
1700        let a = scene
1701            .add_mesh(unit_sphere(), MeshOptions::default())
1702            .unwrap();
1703        let b = scene
1704            .add_mesh(unit_sphere(), with_mode(UpdateMode::Lazy))
1705            .unwrap();
1706        let c = scene
1707            .add_mesh(unit_sphere(), MeshOptions::default())
1708            .unwrap();
1709        let id = scene
1710            .add_section(desc(z_plane(0.2), &[a, b], false))
1711            .unwrap();
1712        scene.take_changes();
1713        let n = scene.section_recomputes;
1714        scene.set_section_meshes(id, vec![c, a]).unwrap();
1715        // c (Eager) is computed, a is kept as it is, b is dropped.
1716        assert_eq!(scene.section_recomputes, n + 1);
1717        assert_eq!(scene.section_status(id, a), Ok(SectionStatus::Current));
1718        assert_eq!(scene.section_status(id, c), Ok(SectionStatus::Current));
1719        assert!(matches!(
1720            scene.section(id, b),
1721            Err(SceneError::InvalidInput(_))
1722        ));
1723        let ch = scene.take_changes();
1724        assert_eq!(of(&ch, a), []);
1725        assert_eq!(of(&ch, b), [id]);
1726        assert_eq!(of(&ch, c), [id]);
1727        assert!(scene.entry(b).unwrap().section_hooks.is_empty());
1728        // A selected Lazy mesh is stale until updated.
1729        scene.set_section_meshes(id, vec![c, a, b]).unwrap();
1730        assert_eq!(scene.section_status(id, b), Ok(SectionStatus::Stale));
1731        assert_fresh(&mut scene, id, b, "reselected");
1732    }
1733
1734    #[test]
1735    fn compact_recomputes_and_reports() {
1736        let (mut scene, m) = one_sphere(UpdateMode::Eager);
1737        let id = scene.add_section(desc(z_plane(0.3), &[m], false)).unwrap();
1738        // Remove the north pole, so that compaction renumbers every vertex.
1739        remove_triangles_using(&mut scene, m, 0);
1740        scene.edit(m, |tx| tx.remove_vertices(&[0])).unwrap();
1741        assert_fresh(&mut scene, id, m, "holed");
1742        scene.take_changes();
1743        let before = scene.section(id, m).unwrap().clone();
1744        let n = scene.section_recomputes;
1745        scene.compact(m).unwrap();
1746        assert_eq!(scene.section_recomputes, n + 1);
1747        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Current));
1748        let after = scene.section(id, m).unwrap();
1749        assert_ne!(after.contours[0].edges, before.contours[0].edges);
1750        assert!(close(after.length, before.length, 1e-12));
1751        let ch = scene.take_changes();
1752        assert!(ch[0].changes.is_none());
1753        assert_eq!(ch[0].sections_changed, [id]);
1754        assert_fresh(&mut scene, id, m, "compacted");
1755    }
1756
1757    #[test]
1758    fn manual_stale_bvh_returns_error_and_stays_stale() {
1759        let (mut scene, m) = one_sphere(UpdateMode::Manual);
1760        let id = scene.add_section(desc(z_plane(0.3), &[m], false)).unwrap();
1761        // Manual waits, even with a current BVH; flush leaves it alone.
1762        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Stale));
1763        scene.flush().unwrap();
1764        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Stale));
1765        scene.update_sections().unwrap();
1766        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Current));
1767        scene
1768            .edit(m, |tx| tx.set_positions(&[40], &[[0.0, 0.0, 0.0]]))
1769            .unwrap();
1770        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Stale));
1771        let old = scene.section(id, m).unwrap().clone();
1772        assert_eq!(scene.update_sections(), Err(SceneError::StaleBvh(m)));
1773        assert_eq!(scene.update_sections(), Err(SceneError::StaleBvh(m)));
1774        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Stale));
1775        assert_eq!(scene.section(id, m), Ok(&old));
1776        scene.flush().unwrap();
1777        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Stale));
1778        scene.update(m).unwrap();
1779        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Current));
1780        assert_fresh(&mut scene, id, m, "updated");
1781        // add_section never fails with StaleBvh.
1782        scene
1783            .edit(m, |tx| tx.set_positions(&[41], &[[0.0, 0.0, 0.0]]))
1784            .unwrap();
1785        let other = scene.add_section(desc(z_plane(0.0), &[m], false)).unwrap();
1786        assert_eq!(scene.section_status(other, m), Ok(SectionStatus::Stale));
1787        assert_eq!(scene.update_sections(), Err(SceneError::StaleBvh(m)));
1788    }
1789
1790    #[test]
1791    fn failed_eager_bvh_update_is_retried_by_the_section() {
1792        let (mut scene, m) = one_sphere(UpdateMode::Eager);
1793        let id = scene.add_section(desc(z_plane(0.3), &[m], false)).unwrap();
1794        // The edit's own BVH update fails; the edit still succeeds, and the
1795        // section brings the BVH up to date before cutting.
1796        crate::scene::fault::fail_bvh_update_on(1);
1797        scene
1798            .edit(m, |tx| tx.set_positions(&[40], &[[0.0, 0.0, 0.0]]))
1799            .unwrap();
1800        assert_eq!(scene.is_current(m), Ok(true));
1801        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Current));
1802        assert_fresh(&mut scene, id, m, "retried");
1803    }
1804
1805    /// The id of vertex `j` of ring `i` of a UV sphere with `slices` slices.
1806    fn ring(slices: u32, i: u32, j: u32) -> VertexId {
1807        1 + (i - 1) * slices + j % slices
1808    }
1809
1810    /// A 64 x 128 unit sphere in `mode`, cut at z = 0.3, its add and first
1811    /// computation taken.
1812    fn fine_sphere_cut(mode: UpdateMode) -> (Scene, MeshId, SectionId) {
1813        let mut scene = Scene::new(SceneOptions::default());
1814        let m = scene
1815            .add_mesh(sphere(64, 128, 1.0, [0.0; 3]), with_mode(mode))
1816            .unwrap();
1817        let id = scene.add_section(desc(z_plane(0.3), &[m], true)).unwrap();
1818        scene.update_sections().unwrap();
1819        scene.take_changes();
1820        (scene, m, id)
1821    }
1822
1823    fn crossed(scene: &Scene, id: SectionId) -> (Vec<VertexId>, Vec<TriangleId>) {
1824        let se = &scene.sections[scene.section_index(id).unwrap()].entries[0];
1825        (se.crossed_vertices.clone(), se.crossed_triangles.clone())
1826    }
1827
1828    fn nudge(scene: &mut Scene, m: MeshId, v: VertexId, d: [f32; 3]) {
1829        scene
1830            .edit(m, |tx| {
1831                let p = tx.mesh().position(v).unwrap();
1832                tx.set_positions(&[v], &[std::array::from_fn(|k| p[k] + d[k])])
1833            })
1834            .unwrap();
1835    }
1836
1837    /// The triangle with exactly the vertices `tri`, in that winding.
1838    fn find_triangle(scene: &Scene, m: MeshId, tri: [VertexId; 3]) -> TriangleId {
1839        let mesh = scene.mesh(m).unwrap();
1840        (0..mesh.triangle_slots() as TriangleId)
1841            .find(|&t| mesh.triangle(t) == Some(tri))
1842            .unwrap()
1843    }
1844
1845    /// Flips the diagonal of the quad of rings `i`, `i + 1` at slice `j`.
1846    fn flip_quad(scene: &mut Scene, m: MeshId, slices: u32, i: u32, j: u32) -> [TriangleId; 2] {
1847        let (a, b, c, d) = (
1848            ring(slices, i, j),
1849            ring(slices, i + 1, j),
1850            ring(slices, i + 1, j + 1),
1851            ring(slices, i, j + 1),
1852        );
1853        let t = [
1854            find_triangle(scene, m, [a, b, c]),
1855            find_triangle(scene, m, [a, c, d]),
1856        ];
1857        scene
1858            .edit(m, |tx| tx.set_triangles(&t, &[[a, b, d], [b, c, d]]))
1859            .unwrap();
1860        t
1861    }
1862
1863    #[test]
1864    fn far_edit_does_not_recompute() {
1865        for mode in [UpdateMode::Eager, UpdateMode::Lazy, UpdateMode::Manual] {
1866            let (mut scene, m, id) = fine_sphere_cut(mode);
1867            let before = scene.section(id, m).unwrap().clone();
1868            let n = scene.section_recomputes;
1869            // Ring 55 sits at z = -0.904.
1870            let v = ring(128, 55, 7);
1871            assert!(scene.mesh(m).unwrap().position(v).unwrap()[2] < -0.9);
1872            nudge(&mut scene, m, v, [0.0, 0.0, 0.01]);
1873            // A far edge flip and a far triangle removal.
1874            flip_quad(&mut scene, m, 128, 50, 3);
1875            let t = find_triangle(&scene, m, [0, ring(128, 1, 9), ring(128, 1, 10)]);
1876            scene.edit(m, |tx| tx.remove_triangles(&[t])).unwrap();
1877            assert_eq!(
1878                scene.section_status(id, m),
1879                Ok(SectionStatus::Current),
1880                "{mode:?}"
1881            );
1882            scene.update_sections().unwrap();
1883            scene.flush().unwrap();
1884            assert_eq!(scene.section_recomputes, n, "{mode:?}");
1885            assert_eq!(scene.section(id, m), Ok(&before));
1886            let ch = scene.take_changes();
1887            assert_eq!(of(&ch, m), [], "{mode:?}");
1888            if mode == UpdateMode::Manual {
1889                scene.update(m).unwrap();
1890            }
1891            assert_fresh(&mut scene, id, m, &format!("{mode:?} far"));
1892        }
1893    }
1894
1895    #[test]
1896    fn edit_touching_plane_recomputes() {
1897        let (mut scene, m, id) = fine_sphere_cut(UpdateMode::Eager);
1898        let n = scene.section_recomputes;
1899        let (cv, _) = crossed(&scene, id);
1900        nudge(&mut scene, m, cv[cv.len() / 2], [0.0, 0.0, 0.004]);
1901        assert_eq!(scene.section_recomputes, n + 1);
1902        assert_eq!(of(&scene.take_changes(), m), [id]);
1903        assert_fresh(&mut scene, id, m, "touching");
1904    }
1905
1906    #[test]
1907    fn vertex_moved_across_plane_recomputes() {
1908        let (mut scene, m, id) = fine_sphere_cut(UpdateMode::Eager);
1909        let n = scene.section_recomputes;
1910        // Ring 27 sits at z = 0.243, below the crossed rings 25 and 26.
1911        let v = ring(128, 27, 40);
1912        let (cv, _) = crossed(&scene, id);
1913        assert!(cv.binary_search(&v).is_err());
1914        let p = scene.mesh(m).unwrap().position(v).unwrap();
1915        assert!((p[2] - 0.25).abs() < 0.01, "{p:?}");
1916        scene
1917            .edit(m, |tx| tx.set_positions(&[v], &[[p[0], p[1], 0.35]]))
1918            .unwrap();
1919        assert_eq!(scene.section_recomputes, n + 1);
1920        assert_eq!(of(&scene.take_changes(), m), [id]);
1921        assert_fresh(&mut scene, id, m, "across");
1922    }
1923
1924    #[test]
1925    fn edge_flip_near_plane_recomputes() {
1926        let (mut scene, m, id) = fine_sphere_cut(UpdateMode::Lazy);
1927        let n = scene.section_recomputes;
1928        let before = scene.section(id, m).unwrap().clone();
1929        let (_, ct) = crossed(&scene, id);
1930        // The quad between the crossed rings 25 and 26.
1931        let t = flip_quad(&mut scene, m, 128, 25, 11);
1932        assert!(t.iter().all(|t| ct.binary_search(t).is_ok()));
1933        assert_eq!(scene.section_status(id, m), Ok(SectionStatus::Stale));
1934        scene.update_sections().unwrap();
1935        assert_eq!(scene.section_recomputes, n + 1);
1936        assert_ne!(scene.section(id, m), Ok(&before));
1937        assert_eq!(of(&scene.take_changes(), m), [id]);
1938        assert_fresh(&mut scene, id, m, "flip");
1939    }
1940
1941    #[test]
1942    fn far_triangles_added_and_vertices_added_do_not_recompute() {
1943        let (mut scene, m, id) = fine_sphere_cut(UpdateMode::Eager);
1944        let n = scene.section_recomputes;
1945        let (p, q) = (ring(128, 60, 0), ring(128, 60, 1));
1946        scene
1947            .edit(m, |tx| {
1948                let v = tx.add_vertices(&[[0.1, 0.1, -0.95]])?[0];
1949                tx.add_triangles(&[[v, p, q]])
1950            })
1951            .unwrap();
1952        assert_eq!(scene.section_recomputes, n);
1953        // A new triangle crossing the plane does recompute.
1954        scene
1955            .edit(m, |tx| {
1956                let v = tx.add_vertices(&[[0.0, 0.0, 0.9]])?[0];
1957                tx.add_triangles(&[[v, p, q]])
1958            })
1959            .unwrap();
1960        assert_eq!(scene.section_recomputes, n + 1);
1961        assert_fresh(&mut scene, id, m, "added");
1962    }
1963
1964    #[test]
1965    fn reaches_flag_skips_repeated_plane_checks() {
1966        // A long sculpt session without update: every edit re-checks the
1967        // whole merged change set, so the re-checks cost quadratic time.
1968        // Moving a different crossed vertex per edit grows the merged set
1969        // every time; the first edit finds it reaches the plane and the
1970        // rest remember it. Eager entries recompute (and reset) per edit,
1971        // so each of their fresh sets is re-checked.
1972        for mode in [UpdateMode::Eager, UpdateMode::Lazy, UpdateMode::Manual] {
1973            let (mut scene, m, id) = fine_sphere_cut(mode);
1974            let (cv, _) = crossed(&scene, id);
1975            scene.section_reach_checks = 0;
1976            for (k, &v) in cv.iter().take(10).enumerate() {
1977                nudge(&mut scene, m, v, [0.0, 0.0, 0.001 * (k as f32 + 1.0)]);
1978                let expect = if mode == UpdateMode::Eager {
1979                    SectionStatus::Current
1980                } else {
1981                    SectionStatus::Stale
1982                };
1983                assert_eq!(scene.section_status(id, m), Ok(expect), "{mode:?}");
1984            }
1985            let expect = if mode == UpdateMode::Eager { 10 } else { 1 };
1986            assert_eq!(scene.section_reach_checks, expect, "{mode:?}");
1987            if mode == UpdateMode::Manual {
1988                scene.update(m).unwrap();
1989            } else {
1990                scene.update_sections().unwrap();
1991            }
1992            assert_eq!(
1993                scene.section_status(id, m),
1994                Ok(SectionStatus::Current),
1995                "{mode:?}"
1996            );
1997            assert_fresh(&mut scene, id, m, &format!("{mode:?} sculpt"));
1998        }
1999    }
2000
2001    #[test]
2002    fn gap_needs_two_meshes_and_one_plane() {
2003        let mut scene = Scene::new(SceneOptions::default());
2004        let a = scene
2005            .add_mesh(unit_sphere(), MeshOptions::default())
2006            .unwrap();
2007        let opts = MeshOptions {
2008            placement: at(3.0, 0.0, 0.0),
2009            mode: UpdateMode::Lazy,
2010            ..MeshOptions::default()
2011        };
2012        let b = scene.add_mesh(unit_sphere(), opts).unwrap();
2013        let id = scene
2014            .add_section(desc(z_plane(0.0), &[a, b], false))
2015            .unwrap();
2016        assert!(matches!(
2017            scene.section_gap(id, a, a),
2018            Err(SceneError::InvalidInput(_))
2019        ));
2020        scene.update_sections().unwrap();
2021        assert!(scene.section_gap(id, a, b).unwrap().is_some());
2022        // a (Eager) is cut at the new plane, b (Lazy) still at the old one.
2023        scene.set_section_plane(id, z_plane(0.1)).unwrap();
2024        assert_eq!(scene.section_gap(id, a, b), Ok(None));
2025        scene.update_sections().unwrap();
2026        let g = scene.section_gap(id, a, b).unwrap().unwrap();
2027        assert!((g.a[2] - 0.1).abs() < 1e-9 && (g.b[2] - 0.1).abs() < 1e-9);
2028    }
2029
2030    #[test]
2031    fn gap_survives_fill_toggle_with_mixed_entries() {
2032        // A fill toggle restales every entry but changes no plane: with `a`
2033        // (Eager) recomputed and `b` (Lazy) still on its last result, the
2034        // computed-with planes are still equal and the gap still defined.
2035        let mut scene = Scene::new(SceneOptions::default());
2036        let a = scene
2037            .add_mesh(unit_sphere(), MeshOptions::default())
2038            .unwrap();
2039        let opts = MeshOptions {
2040            placement: at(3.0, 0.0, 0.0),
2041            mode: UpdateMode::Lazy,
2042            ..MeshOptions::default()
2043        };
2044        let b = scene.add_mesh(unit_sphere(), opts).unwrap();
2045        let id = scene
2046            .add_section(desc(z_plane(0.0), &[a, b], false))
2047            .unwrap();
2048        scene.update_sections().unwrap();
2049        let before = scene.section_gap(id, a, b).unwrap().unwrap();
2050        scene.set_section_fill(id, true).unwrap();
2051        assert_eq!(scene.section_status(id, a), Ok(SectionStatus::Current));
2052        assert_eq!(scene.section_status(id, b), Ok(SectionStatus::Stale));
2053        assert!(scene.section(id, a).unwrap().fill.is_some());
2054        assert!(scene.section(id, b).unwrap().fill.is_none());
2055        assert_eq!(scene.section_plane(id, a), scene.section_plane(id, b));
2056        assert_eq!(scene.section_gap(id, a, b), Ok(Some(before)));
2057        scene.update_sections().unwrap();
2058        assert!(scene.section(id, b).unwrap().fill.is_some());
2059        assert_eq!(
2060            scene.section_gap(id, a, b).unwrap().unwrap().distance,
2061            before.distance
2062        );
2063    }
2064
2065    #[test]
2066    fn section_plane_is_the_plane_computed_with() {
2067        let mut scene = Scene::new(SceneOptions::default());
2068        let a = scene
2069            .add_mesh(unit_sphere(), MeshOptions::default())
2070            .unwrap();
2071        let b = scene
2072            .add_mesh(unit_sphere(), with_mode(UpdateMode::Lazy))
2073            .unwrap();
2074        let c = scene
2075            .add_mesh(unit_sphere(), MeshOptions::default())
2076            .unwrap();
2077        let id = scene
2078            .add_section(desc(z_plane(0.0), &[a, b], true))
2079            .unwrap();
2080        // a (Eager) is computed, b (Lazy) not yet.
2081        assert_eq!(scene.section_plane(id, a), Ok(Some(z_plane(0.0))));
2082        assert_eq!(scene.section_plane(id, b), Ok(None));
2083        scene.set_section_plane(id, z_plane(0.1)).unwrap();
2084        scene.update_sections().unwrap();
2085        scene.set_section_plane(id, z_plane(0.2)).unwrap();
2086        assert_eq!(scene.section_plane(id, a), Ok(Some(z_plane(0.2))));
2087        assert_eq!(scene.section_plane(id, b), Ok(Some(z_plane(0.1))));
2088        // The fill maps to the world through that plane.
2089        let p = scene.section_plane(id, a).unwrap().unwrap();
2090        let fill = scene.section(id, a).unwrap().fill.clone().unwrap();
2091        assert!(fill.points.iter().all(|&q| p.from_uv(q)[2] == 0.2));
2092        assert!(matches!(
2093            scene.section_plane(id, c),
2094            Err(SceneError::InvalidInput(_))
2095        ));
2096        scene.remove_section(id).unwrap();
2097        assert_eq!(scene.section_plane(id, a), Err(SceneError::UnknownSection));
2098    }
2099
2100    #[test]
2101    fn section_revision_counts_result_changes() {
2102        let (mut scene, m) = one_sphere(UpdateMode::Lazy);
2103        let id = scene.add_section(desc(z_plane(0.3), &[m], false)).unwrap();
2104        // Never computed: revision 0.
2105        assert_eq!(scene.section_revision(id, m), Ok(0));
2106        scene.update_sections().unwrap();
2107        assert_eq!(scene.section_revision(id, m), Ok(1));
2108        // A far edit settles without recompute: unchanged.
2109        let n = scene.section_recomputes;
2110        nudge(&mut scene, m, 0, [0.0, 0.0, 0.01]);
2111        scene.update_sections().unwrap();
2112        assert_eq!(scene.section_recomputes, n);
2113        assert_eq!(scene.section_revision(id, m), Ok(1));
2114        // A plane move recomputes with a new result: up by one.
2115        scene.set_section_plane(id, z_plane(-0.5)).unwrap();
2116        scene.update_sections().unwrap();
2117        assert_eq!(scene.section_revision(id, m), Ok(2));
2118        // A recompute with an identical result: unchanged.
2119        scene.set_section_fill(id, false).unwrap();
2120        scene.update_sections().unwrap();
2121        assert_eq!(scene.section_recomputes, n + 2);
2122        assert_eq!(scene.section_revision(id, m), Ok(2));
2123        // An Eager entry is at 1 right after the add computes it.
2124        let (mut eager, e) = one_sphere(UpdateMode::Eager);
2125        let eid = eager.add_section(desc(z_plane(0.3), &[e], false)).unwrap();
2126        assert_eq!(eager.section_revision(eid, e), Ok(1));
2127        // Errors as for `section`.
2128        let other = scene
2129            .add_mesh(unit_sphere(), MeshOptions::default())
2130            .unwrap();
2131        assert!(matches!(
2132            scene.section_revision(id, other),
2133            Err(SceneError::InvalidInput(_))
2134        ));
2135        scene.remove_mesh(other).unwrap();
2136        assert_eq!(
2137            scene.section_revision(id, other),
2138            Err(SceneError::UnknownMesh)
2139        );
2140        scene.remove_section(id).unwrap();
2141        assert_eq!(
2142            scene.section_revision(id, m),
2143            Err(SceneError::UnknownSection)
2144        );
2145    }
2146
2147    #[test]
2148    fn sections_changed_exact() {
2149        let mut scene = Scene::new(SceneOptions::default());
2150        let a = scene
2151            .add_mesh(unit_sphere(), MeshOptions::default())
2152            .unwrap();
2153        let b = scene
2154            .add_mesh(unit_sphere(), MeshOptions::default())
2155            .unwrap();
2156        let c = scene
2157            .add_mesh(unit_sphere(), MeshOptions::default())
2158            .unwrap();
2159        let s1 = scene
2160            .add_section(desc(z_plane(0.2), &[a, b], false))
2161            .unwrap();
2162        let s2 = scene
2163            .add_section(desc(z_plane(0.4), &[b, c], false))
2164            .unwrap();
2165        let ch = scene.take_changes();
2166        assert_eq!(of(&ch, a), [s1]);
2167        assert_eq!(of(&ch, b), [s1, s2]);
2168        assert_eq!(of(&ch, c), [s2]);
2169        // Two plane moves: s1 once, on its meshes only.
2170        scene.set_section_plane(s1, z_plane(0.1)).unwrap();
2171        scene.set_section_plane(s1, z_plane(0.0)).unwrap();
2172        let ch = scene.take_changes();
2173        assert_eq!(ch.len(), 2);
2174        assert_eq!(of(&ch, a), [s1]);
2175        assert_eq!(of(&ch, b), [s1]);
2176        assert_eq!(of(&ch, c), []);
2177        // A placement change of b: both its sections, on b only.
2178        scene.set_placement(b, at(0.0, 0.0, 0.05)).unwrap();
2179        let ch = scene.take_changes();
2180        assert_eq!(ch.len(), 1);
2181        assert_eq!(of(&ch, b), [s1, s2]);
2182    }
2183
2184    /// Four sections over three overlapping spheres of `stacks` x `2 stacks`
2185    /// cells in `mode`, the plane of each dragged through four poses; per
2186    /// pose, the reports and every result and gap, printed in full (so equal
2187    /// strings mean equal bits).
2188    fn section_drag(stacks: u32, mode: UpdateMode) -> Vec<String> {
2189        let mut scene = Scene::new(SceneOptions::default());
2190        let meshes: Vec<MeshId> = (0..3)
2191            .map(|k| {
2192                let c = [0.6 * k as f64, 0.1 * k as f64, 0.0];
2193                let mesh = sphere(stacks, 2 * stacks, 1.0, c);
2194                scene.add_mesh(mesh, with_mode(mode)).unwrap()
2195            })
2196            .collect();
2197        let normals = [
2198            [0.0, 0.0, 1.0],
2199            [1.0, 0.0, 0.0],
2200            [0.3, 1.0, 0.2],
2201            [1.0, 1.0, 1.0],
2202        ];
2203        let plane = |s: usize, pose: usize| {
2204            let t = 0.05 * pose as f64 - 0.1 + 0.02 * s as f64;
2205            Plane::new([0.6 + t, t, t], normals[s]).unwrap()
2206        };
2207        let sections: Vec<SectionId> = (0..4)
2208            .map(|s| {
2209                let fill = s % 2 == 0;
2210                scene.add_section(desc(plane(s, 0), &meshes, fill)).unwrap()
2211            })
2212            .collect();
2213        scene.update_sections().unwrap();
2214        scene.take_changes();
2215        let mut out = Vec::new();
2216        for pose in 1..5 {
2217            for (s, &id) in sections.iter().enumerate() {
2218                scene.set_section_plane(id, plane(s, pose)).unwrap();
2219            }
2220            scene.update_sections().unwrap();
2221            let mut seen = format!("{:?}", scene.take_changes());
2222            for &id in &sections {
2223                for &m in &meshes {
2224                    let r = scene.section(id, m).unwrap();
2225                    assert!(!r.contours.is_empty());
2226                    seen += &format!("{r:?}");
2227                }
2228                seen += &format!("{:?}", scene.section_gap(id, meshes[0], meshes[2]));
2229            }
2230            out.push(seen);
2231        }
2232        out
2233    }
2234
2235    #[test]
2236    fn sections_bit_identical() {
2237        use fast_mesh_store::par;
2238        for mode in [UpdateMode::Eager, UpdateMode::Lazy] {
2239            // The forced-sequential run is the code a feature-off build runs.
2240            par::set_force_sequential(true);
2241            let reference = section_drag(40, mode);
2242            par::set_force_sequential(false);
2243            if !par::enabled() {
2244                // Every drag below would be this one again.
2245                continue;
2246            }
2247            for k in [1, 2, 8] {
2248                let got = par::with_threads(k, || section_drag(40, mode));
2249                assert!(got == reference, "{mode:?}: {k} threads");
2250            }
2251            let batches = PARALLEL_SECTION_BATCHES.with(Cell::get);
2252            assert!(section_drag(40, mode) == reference, "{mode:?}: global pool");
2253            let ran = PARALLEL_SECTION_BATCHES.with(Cell::get) > batches;
2254            assert!(ran, "{mode:?}: ran sequentially");
2255        }
2256    }
2257
2258    #[test]
2259    fn small_inputs_stay_sequential() {
2260        use fast_mesh_store::par;
2261        let before = par::parallel_entries();
2262        let batches = PARALLEL_SECTION_BATCHES.with(Cell::get);
2263        for mode in [UpdateMode::Eager, UpdateMode::Lazy] {
2264            section_drag(8, mode);
2265        }
2266        assert_eq!(par::parallel_entries(), before);
2267        assert_eq!(PARALLEL_SECTION_BATCHES.with(Cell::get), batches);
2268    }
2269}