git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

src/detangle.rs (66.4K)

   1 //! The Detangle node's solve: push a surface off itself.
   2 //!
   3 //! The ALGORITHM is `geometry::apply_detangle`'s, unchanged, and what it
   4 //! does and why is said there. This module is how it is run: what it costs
   5 //! is a function of four things the first version paid for every step of a
   6 //! simulation, and none of which a step needs to pay for.
   7 //!
   8 //! - **The topology is built once.** The edge list and each point's excluded
   9 //!   neighbourhood are connectivity, and a chain of pull, relax and detangle
  10 //!   never changes connectivity — but every step arrives as a fresh
  11 //!   `Detail`, whose derived topology is deliberately not cloned. They are
  12 //!   kept here by a hash of the primitives ([`Topo`]), so a solve of a
  13 //!   hundred steps builds them on the first.
  14 //! - **A pass that separates nothing ends the solve.** The passes gather
  15 //!   against the positions at their start; if one moves nothing, the next
  16 //!   starts from the same positions and finds the same nothing.
  17 //! - **The grid is built once and reused** while the points stay near where
  18 //!   it filed them, in a flat array sorted by cell rather than a vector per
  19 //!   cell. A point that has moved is still found: the search reaches as far
  20 //!   as anything has moved since the grid was built.
  21 //! - **A point that cannot move is not searched for.** With a Group, what
  22 //!   would push the others was worked out and thrown away.
  23 //!
  24 //! The results are the first version's BIT FOR BIT, which is what lets these
  25 //! be optimizations and not changes: the same pairs, summed in the same
  26 //! order. `the_detangle_solve_matches_its_reference` holds them together.
  27 //!
  28 //! That is the node's `Points` method. Two things here are NOT the first
  29 //! version's, and are asked for by name:
  30 //!
  31 //! - **The `Surface` method** ([`solve_surface`]) tests each point against
  32 //!   the TRIANGLES near it, where Points tests it against points. A point
  33 //!   over the middle of a triangle is near no corner of it, so on a mesh
  34 //!   whose triangles are larger than the thickness the point test sees
  35 //!   nothing at all. Told where the points were when the step began
  36 //!   ([`apply_from`]), it also knows which side of a triangle a point
  37 //!   belongs on, and can hold a step to a length.
  38 //! - **The measure** ([`self_intersections`]): every edge that passes
  39 //!   through a triangle. It is what says whether a change to the solve
  40 //!   helped, and what the node's `Tangled Group` is written from.
  41 
  42 use crate::app::FsNode;
  43 use crate::detail::Detail;
  44 use crate::geometry::{node_param_f32, node_param_str};
  45 use glam::Vec3;
  46 use std::cell::RefCell;
  47 use std::rc::Rc;
  48 
  49 /// What a mesh's connectivity gives the solve, for one ring count.
  50 struct Topo {
  51     key: u64,
  52     rings: usize,
  53     edges: Vec<[u32; 2]>,
  54     /// The primitives as triangles, fanned as `Detail::triangulate` fans
  55     /// them, and the primitive each came from.
  56     tris: Vec<[u32; 3]>,
  57     tri_prims: Vec<u32>,
  58     /// Every side of every triangle, once, and each triangle's three. The
  59     /// mesh's edges and the diagonals a fan cuts across a quad: what the
  60     /// triangles are made of, which is what can pass through itself.
  61     sides: Vec<[u32; 2]>,
  62     tri_sides: Vec<[u32; 3]>,
  63     /// The triangles at each point, and the sides: point `p`'s are
  64     /// `at_tris[tri_starts[p]..tri_starts[p + 1]]`, and likewise.
  65     tri_starts: Vec<u32>,
  66     at_tris: Vec<u32>,
  67     side_starts: Vec<u32>,
  68     at_sides: Vec<u32>,
  69     /// Each point's excluded neighbourhood, itself included, ascending:
  70     /// point `p`'s is `excluded[starts[p]..starts[p + 1]]`.
  71     starts: Vec<u32>,
  72     excluded: Vec<u32>,
  73 }
  74 
  75 impl Topo {
  76     fn build(geom: &Detail, key: u64, rings: usize) -> Topo {
  77         let n = geom.num_points();
  78         let mut starts = Vec::with_capacity(n + 1);
  79         let mut excluded = Vec::new();
  80         let mut seen: Vec<u32> = Vec::new();
  81         let mut frontier: Vec<u32> = Vec::new();
  82         let mut next: Vec<u32> = Vec::new();
  83         // One mark per point instead of a search of `seen` per neighbour.
  84         let mut mark = vec![u32::MAX; n];
  85         for p in 0..n {
  86             starts.push(excluded.len() as u32);
  87             seen.clear();
  88             frontier.clear();
  89             seen.push(p as u32);
  90             frontier.push(p as u32);
  91             mark[p] = p as u32;
  92             for _ in 0..rings {
  93                 next.clear();
  94                 for &q in &frontier {
  95                     for &r in geom.point_neighbours(q as usize) {
  96                         if mark[r as usize] != p as u32 {
  97                             mark[r as usize] = p as u32;
  98                             seen.push(r);
  99                             next.push(r);
 100                         }
 101                     }
 102                 }
 103                 if next.is_empty() {
 104                     break;
 105                 }
 106                 std::mem::swap(&mut frontier, &mut next);
 107             }
 108             seen.sort_unstable();
 109             excluded.extend_from_slice(&seen);
 110         }
 111         starts.push(excluded.len() as u32);
 112         let mut tris = Vec::new();
 113         let mut tri_prims = Vec::new();
 114         for prim in 0..geom.num_prims() {
 115             let pts = geom.prim_points(prim);
 116             for i in 1..pts.len().saturating_sub(1) {
 117                 tris.push([pts[0], pts[i], pts[i + 1]]);
 118                 tri_prims.push(prim as u32);
 119             }
 120         }
 121         let mut sides: Vec<[u32; 2]> = Vec::new();
 122         let mut side_of: std::collections::HashMap<[u32; 2], u32> = std::collections::HashMap::new();
 123         let tri_sides = tris
 124             .iter()
 125             .map(|t| {
 126                 [[t[0], t[1]], [t[1], t[2]], [t[2], t[0]]].map(|[a, b]| {
 127                     let ends = [a.min(b), a.max(b)];
 128                     *side_of.entry(ends).or_insert_with(|| {
 129                         sides.push(ends);
 130                         sides.len() as u32 - 1
 131                     })
 132                 })
 133             })
 134             .collect();
 135         let tri_sides: Vec<[u32; 3]> = tri_sides;
 136         let (tri_starts, at_tris) = by_point(n, tris.iter().map(|t| &t[..]));
 137         let (side_starts, at_sides) = by_point(n, sides.iter().map(|e| &e[..]));
 138         Topo { key, rings, edges: geom.edges().to_vec(), tris, tri_prims, sides, tri_sides, tri_starts, at_tris, side_starts, at_sides, starts, excluded }
 139     }
 140 
 141     fn own(&self, p: usize) -> &[u32] {
 142         &self.excluded[self.starts[p] as usize..self.starts[p + 1] as usize]
 143     }
 144 
 145     fn tris_at(&self, p: usize) -> &[u32] {
 146         &self.at_tris[self.tri_starts[p] as usize..self.tri_starts[p + 1] as usize]
 147     }
 148 
 149     fn sides_at(&self, p: usize) -> &[u32] {
 150         &self.at_sides[self.side_starts[p] as usize..self.side_starts[p + 1] as usize]
 151     }
 152 
 153     /// Whether `q`, or a point a side away from it, is stirred: the other
 154     /// end of a pair reaches one ring past the rings.
 155     fn neighbours_stirred(&self, q: u32, stirred: &[bool]) -> bool {
 156         stirred[q as usize] || self.sides_at(q as usize).iter().any(|&j| self.sides[j as usize].iter().any(|&r| stirred[r as usize]))
 157     }
 158 
 159     fn excludes(&self, p: usize, q: u32) -> bool {
 160         let (a, b) = (self.starts[p] as usize, self.starts[p + 1] as usize);
 161         self.excluded[a..b].binary_search(&q).is_ok()
 162     }
 163 }
 164 
 165 /// Which of `items` each point is part of, flat: point `p`'s are
 166 /// `ids[starts[p]..starts[p + 1]]`, ascending.
 167 fn by_point<'a>(n: usize, items: impl Iterator<Item = &'a [u32]> + Clone) -> (Vec<u32>, Vec<u32>) {
 168     let mut starts = vec![0u32; n + 1];
 169     for item in items.clone() {
 170         for &p in item {
 171             starts[p as usize + 1] += 1;
 172         }
 173     }
 174     for p in 0..n {
 175         starts[p + 1] += starts[p];
 176     }
 177     let mut next = starts.clone();
 178     let mut ids = vec![0u32; starts[n] as usize];
 179     for (i, item) in items.enumerate() {
 180         for &p in item {
 181             ids[next[p as usize] as usize] = i as u32;
 182             next[p as usize] += 1;
 183         }
 184     }
 185     (starts, ids)
 186 }
 187 
 188 /// How many topologies are kept: a project has a few detangles at most, and
 189 /// each is asked for in turn as a frame's graph is walked.
 190 const KEPT: usize = 4;
 191 
 192 thread_local! {
 193     static TOPOS: RefCell<Vec<Rc<Topo>>> = const { RefCell::new(Vec::new()) };
 194 }
 195 
 196 /// The connectivity's identity: the point count and every primitive's
 197 /// points, in order. Positions are not in it.
 198 fn topology_key(geom: &Detail) -> u64 {
 199     // FNV-1a over the indices: a few thousand words a step, and nothing
 200     // here needs a keyed hash.
 201     let mut h: u64 = 0xcbf29ce484222325;
 202     let mut eat = |v: u32| {
 203         for b in v.to_le_bytes() {
 204             h ^= b as u64;
 205             h = h.wrapping_mul(0x100000001b3);
 206         }
 207     };
 208     eat(geom.num_points() as u32);
 209     eat(geom.num_prims() as u32);
 210     for prim in 0..geom.num_prims() {
 211         let points = geom.prim_points(prim);
 212         eat(points.len() as u32);
 213         for &p in points {
 214             eat(p);
 215         }
 216     }
 217     h
 218 }
 219 
 220 fn topo_for(geom: &Detail, rings: usize) -> Rc<Topo> {
 221     let key = topology_key(geom);
 222     TOPOS.with(|kept| {
 223         let mut kept = kept.borrow_mut();
 224         if let Some(i) = kept.iter().position(|t| t.key == key && t.rings == rings && t.starts.len() == geom.num_points() + 1) {
 225             // Most recent last, so the one dropped is the one longest unused.
 226             let hit = kept.remove(i);
 227             kept.push(hit.clone());
 228             return hit;
 229         }
 230         let built = Rc::new(Topo::build(geom, key, rings));
 231         if kept.len() >= KEPT {
 232             kept.remove(0);
 233         }
 234         kept.push(built.clone());
 235         built
 236     })
 237 }
 238 
 239 /// How many topologies are kept right now — for the tests.
 240 #[cfg(test)]
 241 pub(crate) fn kept_topologies() -> usize {
 242     TOPOS.with(|k| k.borrow().len())
 243 }
 244 
 245 /// Points filed by cell, flat: `ids[starts[c]..starts[c + 1]]` are the
 246 /// points the grid filed in cell `c`, ascending.
 247 struct FlatGrid {
 248     min: Vec3,
 249     cell: f32,
 250     dims: [i32; 3],
 251     starts: Vec<u32>,
 252     ids: Vec<u32>,
 253     /// Where each point was when it was filed.
 254     filed: Vec<Vec3>,
 255 }
 256 
 257 impl FlatGrid {
 258     fn build(points: &[Vec3], cell: f32) -> FlatGrid {
 259         let (min, max) = points.iter().fold(
 260             (Vec3::splat(f32::MAX), Vec3::splat(f32::MIN)),
 261             |(lo, hi), &p| (lo.min(p), hi.max(p)),
 262         );
 263         let (min, max) = if points.is_empty() { (Vec3::ZERO, Vec3::ZERO) } else { (min, max) };
 264         // The shape `spatial::Grid` gives itself: cells about `cell` across,
 265         // capped so a pathological request cannot ask for a billion of them.
 266         let span = (max - min).max(Vec3::splat(1e-6));
 267         let cell = cell.max(span.max_element() / 128.0).max(1e-6);
 268         let dims = [
 269             ((span.x / cell).ceil() as i32 + 1).clamp(1, 256),
 270             ((span.y / cell).ceil() as i32 + 1).clamp(1, 256),
 271             ((span.z / cell).ceil() as i32 + 1).clamp(1, 256),
 272         ];
 273         let mut grid = FlatGrid { min, cell, dims, starts: Vec::new(), ids: Vec::new(), filed: points.to_vec() };
 274         let cells = (dims[0] * dims[1] * dims[2]) as usize;
 275         // A counting sort: count, prefix-sum, place. Placing in point order
 276         // leaves each cell's ids ascending.
 277         let of: Vec<u32> = points.iter().map(|&p| grid.index(grid.coord(p)) as u32).collect();
 278         let mut starts = vec![0u32; cells + 1];
 279         for &c in &of {
 280             starts[c as usize + 1] += 1;
 281         }
 282         for c in 0..cells {
 283             starts[c + 1] += starts[c];
 284         }
 285         let mut next = starts.clone();
 286         let mut ids = vec![0u32; points.len()];
 287         for (i, &c) in of.iter().enumerate() {
 288             ids[next[c as usize] as usize] = i as u32;
 289             next[c as usize] += 1;
 290         }
 291         grid.starts = starts;
 292         grid.ids = ids;
 293         grid
 294     }
 295 
 296     fn coord(&self, p: Vec3) -> [i32; 3] {
 297         let rel = (p - self.min) / self.cell;
 298         [
 299             (rel.x.floor() as i32).clamp(0, self.dims[0] - 1),
 300             (rel.y.floor() as i32).clamp(0, self.dims[1] - 1),
 301             (rel.z.floor() as i32).clamp(0, self.dims[2] - 1),
 302         ]
 303     }
 304 
 305     fn index(&self, c: [i32; 3]) -> usize {
 306         ((c[2] * self.dims[1] + c[1]) * self.dims[0] + c[0]) as usize
 307     }
 308 
 309     /// Every point filed in a cell the box about `p` touches, ascending.
 310     fn gather(&self, p: Vec3, reach: f32, out: &mut Vec<u32>) {
 311         out.clear();
 312         let (a, b) = (self.coord(p - Vec3::splat(reach)), self.coord(p + Vec3::splat(reach)));
 313         for z in a[2]..=b[2] {
 314             for y in a[1]..=b[1] {
 315                 for x in a[0]..=b[0] {
 316                     let c = self.index([x, y, z]);
 317                     out.extend_from_slice(&self.ids[self.starts[c] as usize..self.starts[c + 1] as usize]);
 318                 }
 319             }
 320         }
 321         // Cells are disjoint, so there is nothing to deduplicate; the order
 322         // is what the sum over a point's pairs is taken in.
 323         out.sort_unstable();
 324     }
 325 
 326     /// How far the furthest point has come from where it was filed.
 327     fn drift(&self, points: &[Vec3]) -> f32 {
 328         points.iter().zip(&self.filed).map(|(p, f)| (*p - *f).length_squared()).fold(0.0f32, f32::max).sqrt()
 329     }
 330 }
 331 
 332 /// How far points may drift from where the grid filed them, in cells,
 333 /// before it is built again. The search reaches a drift further than the
 334 /// thickness, so a larger allowance trades rebuilds for wider searches.
 335 const DRIFT_CELLS: f32 = 0.5;
 336 
 337 /// What a solve did, for the tests and the timing.
 338 #[derive(Debug, Default, Clone, Copy, PartialEq)]
 339 pub struct Work {
 340     pub passes: usize,
 341     pub grids: usize,
 342     pub searched: usize,
 343     /// Point-triangle contacts the Surface method resolved, over every pass.
 344     pub contacts: usize,
 345     /// Edges tested against the edges near them, over every pass: the
 346     /// ones with an end near something that is not their own neighbourhood.
 347     pub edges_searched: usize,
 348     /// Of those contacts, the ones between two EDGES.
 349     pub edge_contacts: usize,
 350     /// Of those contacts, the ones resolved as having gone THROUGH since
 351     /// the step began — a point through a triangle, an edge through an
 352     /// edge — and put back on the side they came from.
 353     pub crossed: usize,
 354     /// Pairs inside each other's rings found gone through each other when
 355     /// the solve began: folds.
 356     pub folds: usize,
 357     /// Points put back where the step began, being still through a
 358     /// triangle when the passes were done, or a corner of one.
 359     pub held: usize,
 360     /// Points whose move since the step began was cut to the Step Limit.
 361     pub limited: usize,
 362     /// Edges passing through a triangle when the solve was done — counted
 363     /// only when the node names a Tangled Group to write them to.
 364     pub crossings: usize,
 365 }
 366 
 367 pub fn apply(geom: &mut Detail, target: &FsNode) -> Work {
 368     apply_from(geom, None, target)
 369 }
 370 
 371 /// [`apply`], told where the points were when the step began: inside a
 372 /// simnet, the state the substep consumed. It is what the Surface method
 373 /// knows a point's SIDE from, and what the Step Limit is measured against.
 374 /// A `before` that is not this mesh — another point count, other
 375 /// primitives — is no memory of it and is not used.
 376 pub fn apply_from(geom: &mut Detail, before: Option<&Detail>, target: &FsNode) -> Work {
 377     let mut work = Work::default();
 378     let n = geom.num_points();
 379     if n == 0 || geom.num_prims() == 0 {
 380         return work;
 381     }
 382     let rings = node_param_f32(target, "rings", 2.0).clamp(0.0, 6.0) as usize;
 383     let topo = topo_for(geom, rings);
 384     if topo.edges.is_empty() {
 385         return work;
 386     }
 387     // A node without the row is one from before it, and solves as it did.
 388     if node_param_str(target, "method", "Points").trim().eq_ignore_ascii_case("Surface") {
 389         let before: Option<Vec<Vec3>> = before
 390             .filter(|b| b.num_points() == n && b.num_prims() == geom.num_prims() && topology_key(b) == topo.key)
 391             .map(|b| (0..n).map(|p| b.pos(p)).collect());
 392         solve_surface(geom, before.as_deref(), target, &topo, &mut work);
 393     } else {
 394         solve_points(geom, target, &topo, &mut work);
 395     }
 396     // What is STILL crossed once the solve is done, which is the part worth
 397     // looking at. Only when asked for: it is a second search of the mesh.
 398     let mark = node_param_str(target, "tangled_group", "");
 399     let mark = mark.trim();
 400     if !mark.is_empty() {
 401         let found = intersections_of(geom, &topo, false);
 402         work.crossings = found.crossings;
 403         geom.points_mut().create_group(mark);
 404         for p in found.points {
 405             geom.points_mut().add_to_group(mark, p as usize);
 406         }
 407     }
 408     work
 409 }
 410 
 411 /// The node's thickness as a length: the setting is in EDGE LENGTHS, so it
 412 /// means the same thing before and after a remesh.
 413 fn thickness_of(geom: &Detail, target: &FsNode, topo: &Topo) -> f32 {
 414     let mean_edge = mean_edge(geom, topo);
 415     node_param_f32(target, "thickness", 1.0).max(0.0) * mean_edge
 416 }
 417 
 418 fn mean_edge(geom: &Detail, topo: &Topo) -> f32 {
 419     topo.edges
 420         .iter()
 421         .map(|e| (geom.pos(e[1] as usize) - geom.pos(e[0] as usize)).length())
 422         .sum::<f32>()
 423         / topo.edges.len() as f32
 424 }
 425 
 426 /// The Points method: the first version's solve.
 427 fn solve_points(geom: &mut Detail, target: &FsNode, topo: &Topo, work: &mut Work) {
 428     let n = geom.num_points();
 429     let thickness = thickness_of(geom, target, topo);
 430     if thickness <= 0.0 {
 431         return;
 432     }
 433     let iterations = node_param_f32(target, "iterations", 4.0).clamp(1.0, 32.0) as usize;
 434     let group = node_param_str(target, "group", "");
 435     let group = group.trim().to_string();
 436     let movable: Vec<bool> = (0..n).map(|p| group.is_empty() || geom.points().in_group(&group, p)).collect();
 437 
 438     let mut pos: Vec<Vec3> = (0..n).map(|p| geom.pos(p)).collect();
 439     let mut grid = FlatGrid::build(&pos, thickness);
 440     work.grids += 1;
 441     let mut drift = 0.0f32;
 442     let mut near = Vec::new();
 443     let mut push = vec![Vec3::ZERO; n];
 444     let mut moved_at_all = false;
 445     for _ in 0..iterations {
 446         work.passes += 1;
 447         if drift > DRIFT_CELLS * grid.cell {
 448             grid = FlatGrid::build(&pos, thickness);
 449             work.grids += 1;
 450             drift = 0.0;
 451         }
 452         // Gathered against the positions at the START of the pass and
 453         // applied at the end, so the result does not depend on the order
 454         // points are visited in.
 455         let mut any = false;
 456         for p in 0..n {
 457             push[p] = Vec3::ZERO;
 458             if !movable[p] {
 459                 continue;
 460             }
 461             work.searched += 1;
 462             // Anything within a thickness of here now was filed within a
 463             // thickness and a drift of here.
 464             grid.gather(pos[p], thickness + drift, &mut near);
 465             for &q in &near {
 466                 let qi = q as usize;
 467                 if qi == p || topo.excludes(p, q) {
 468                     continue;
 469                 }
 470                 let d = pos[p] - pos[qi];
 471                 let len = d.length();
 472                 if len >= thickness {
 473                     continue;
 474                 }
 475                 // Two coincident points have no direction to separate along;
 476                 // nudging along an arbitrary axis at least breaks the tie.
 477                 let dir = if len < 1e-9 {
 478                     Vec3::new((p % 7) as f32 - 3.0, (p % 5) as f32 - 2.0, 1.0).normalize_or_zero()
 479                 } else {
 480                     d / len
 481                 };
 482                 push[p] += dir * ((thickness - len) * 0.5);
 483                 any = true;
 484             }
 485         }
 486         if !any {
 487             // Nothing is within a thickness of anything it may push: the
 488             // next pass would start from these positions and find the same.
 489             break;
 490         }
 491         for p in 0..n {
 492             if movable[p] {
 493                 pos[p] += push[p];
 494             }
 495         }
 496         moved_at_all = true;
 497         drift = grid.drift(&pos);
 498     }
 499     if moved_at_all {
 500         for (p, v) in pos.iter().enumerate() {
 501             geom.set_pos(p, *v);
 502         }
 503     }
 504 }
 505 
 506 /// Triangles filed by cell, flat, as [`FlatGrid`] files points: a triangle
 507 /// is in every cell its bounding box touched when it was filed.
 508 struct TriCells {
 509     min: Vec3,
 510     cell: f32,
 511     dims: [i32; 3],
 512     starts: Vec<u32>,
 513     ids: Vec<u32>,
 514 }
 515 
 516 impl TriCells {
 517     fn build(points: &[Vec3], tris: &[[u32; 3]], cell: f32) -> TriCells {
 518         let (min, max) = points.iter().fold(
 519             (Vec3::splat(f32::MAX), Vec3::splat(f32::MIN)),
 520             |(lo, hi), &p| (lo.min(p), hi.max(p)),
 521         );
 522         let (min, max) = if points.is_empty() { (Vec3::ZERO, Vec3::ZERO) } else { (min, max) };
 523         let span = (max - min).max(Vec3::splat(1e-6));
 524         let cell = cell.max(span.max_element() / 128.0).max(1e-6);
 525         let dims = [
 526             ((span.x / cell).ceil() as i32 + 1).clamp(1, 256),
 527             ((span.y / cell).ceil() as i32 + 1).clamp(1, 256),
 528             ((span.z / cell).ceil() as i32 + 1).clamp(1, 256),
 529         ];
 530         let mut grid = TriCells { min, cell, dims, starts: Vec::new(), ids: Vec::new() };
 531         let cells = (dims[0] * dims[1] * dims[2]) as usize;
 532         let boxes: Vec<([i32; 3], [i32; 3])> = tris
 533             .iter()
 534             .map(|t| {
 535                 let [a, b, c] = t.map(|i| points[i as usize]);
 536                 (grid.coord(a.min(b).min(c)), grid.coord(a.max(b).max(c)))
 537             })
 538             .collect();
 539         // The counting sort again, a triangle counted once per cell it is
 540         // in. Placing in triangle order leaves each cell's ids ascending.
 541         let mut starts = vec![0u32; cells + 1];
 542         for (a, b) in &boxes {
 543             for z in a[2]..=b[2] {
 544                 for y in a[1]..=b[1] {
 545                     for x in a[0]..=b[0] {
 546                         starts[grid.index([x, y, z]) + 1] += 1;
 547                     }
 548                 }
 549             }
 550         }
 551         for c in 0..cells {
 552             starts[c + 1] += starts[c];
 553         }
 554         let mut next = starts.clone();
 555         let mut ids = vec![0u32; starts[cells] as usize];
 556         for (i, (a, b)) in boxes.iter().enumerate() {
 557             for z in a[2]..=b[2] {
 558                 for y in a[1]..=b[1] {
 559                     for x in a[0]..=b[0] {
 560                         let c = grid.index([x, y, z]);
 561                         ids[next[c] as usize] = i as u32;
 562                         next[c] += 1;
 563                     }
 564                 }
 565             }
 566         }
 567         grid.starts = starts;
 568         grid.ids = ids;
 569         grid
 570     }
 571 
 572     fn coord(&self, p: Vec3) -> [i32; 3] {
 573         let rel = (p - self.min) / self.cell;
 574         [
 575             (rel.x.floor() as i32).clamp(0, self.dims[0] - 1),
 576             (rel.y.floor() as i32).clamp(0, self.dims[1] - 1),
 577             (rel.z.floor() as i32).clamp(0, self.dims[2] - 1),
 578         ]
 579     }
 580 
 581     fn index(&self, c: [i32; 3]) -> usize {
 582         ((c[2] * self.dims[1] + c[1]) * self.dims[0] + c[0]) as usize
 583     }
 584 
 585     /// Every triangle filed in a cell the box touches, once, in the order
 586     /// the cells are walked. `seen` is one mark per triangle and `stamp`
 587     /// this gather's, which is cheaper than sorting what came back to find
 588     /// the triangles that came back twice.
 589     fn gather(&self, lo: Vec3, hi: Vec3, seen: &mut [u32], stamp: u32, out: &mut Vec<u32>) {
 590         out.clear();
 591         let (a, b) = (self.coord(lo), self.coord(hi));
 592         for z in a[2]..=b[2] {
 593             for y in a[1]..=b[1] {
 594                 for x in a[0]..=b[0] {
 595                     let c = self.index([x, y, z]);
 596                     for &t in &self.ids[self.starts[c] as usize..self.starts[c + 1] as usize] {
 597                         if seen[t as usize] != stamp {
 598                             seen[t as usize] = stamp;
 599                             out.push(t);
 600                         }
 601                     }
 602                 }
 603             }
 604         }
 605     }
 606 }
 607 
 608 /// The Surface method: each point against the triangles near it.
 609 ///
 610 /// A contact is a point closer than the thickness to a triangle none of
 611 /// whose corners is in the point's excluded rings. It is resolved along the
 612 /// line from the closest point on the triangle to the point — the triangle's
 613 /// own normal where the point lies ON it, which is a direction, where two
 614 /// coincident points have none — and the move is SHARED: the point takes
 615 /// its part one way and the triangle's corners theirs the other, each corner
 616 /// by how much of the closest point it is. What cannot move (outside the
 617 /// Group) takes none and the rest take all of it, so a contact with a fixed
 618 /// triangle is resolved whole, where Points resolves half of it.
 619 ///
 620 /// A pass is gathered against the positions at its start and applied at its
 621 /// end, as Points is. What a point receives from several contacts is their
 622 /// AVERAGE, weighted by how deep each is: a point over a shared edge is in
 623 /// contact with both triangles and must move once, not twice, and a sum is
 624 /// what makes a dense contact overshoot and ring.
 625 ///
 626 /// **With `before`, a point has a side.** Distance alone cannot tell a
 627 /// point that is near a triangle from one that has gone through it, and
 628 /// pushes the second further through. Where the point was on one side of a
 629 /// triangle when the step began and is on the other now, having passed
 630 /// through the triangle's own extent ([`went_through`]), the contact is
 631 /// resolved along the triangle's normal back to the side it came from, to a
 632 /// thickness clear of it. A point with such a contact takes no other in
 633 /// that pass: the triangles beside the one it went through see it near and
 634 /// on the wrong side, and would push it on.
 635 ///
 636 /// The memory is one step long. A point the passes did not bring back is,
 637 /// to the next step, a point that began on that side.
 638 ///
 639 /// **Edges meet edges.** Two edges can pass through each other with no
 640 /// point of either going through any triangle, and then every test above
 641 /// reports nothing while the measure counts a crossing. With Edge Contact
 642 /// on, each side of each triangle is tested against the sides near it
 643 /// that share no neighbourhood with it: where the two are nearest at a
 644 /// place INSIDE both — an end is a point, and a point near an edge is near
 645 /// the edge's triangle, which is the test above — they are parted along
 646 /// the line between those places, the four ends sharing the move by how
 647 /// near each is to it. Told where the step began, two edges that have
 648 /// passed through each other ([`edges_went_through`]) are put back, and
 649 /// held if the passes leave them through.
 650 ///
 651 /// **The Step Limit** cuts each point's move since the step began to that
 652 /// many thicknesses before anything is resolved, so what arrives here is
 653 /// close to what left and a contact is met while it is still a contact.
 654 fn solve_surface(geom: &mut Detail, before: Option<&[Vec3]>, target: &FsNode, topo: &Topo, work: &mut Work) {
 655     let n = geom.num_points();
 656     let thickness = thickness_of(geom, target, topo);
 657     if thickness <= 0.0 || topo.tris.is_empty() {
 658         return;
 659     }
 660     let iterations = node_param_f32(target, "iterations", 4.0).clamp(1.0, 32.0) as usize;
 661     // A node without the row is one from before it: points and triangles.
 662     let edges_too = crate::geometry::node_param_bool(target, "edge_contact", false);
 663     let group = node_param_str(target, "group", "");
 664     let group = group.trim().to_string();
 665     let movable: Vec<bool> = (0..n).map(|p| group.is_empty() || geom.points().in_group(&group, p)).collect();
 666     let free = |p: usize| if movable[p] { 1.0f32 } else { 0.0 };
 667 
 668     let mut pos: Vec<Vec3> = (0..n).map(|p| geom.pos(p)).collect();
 669     let mut moved_at_all = false;
 670     // A node without the row is one from before it, and is not limited.
 671     let limit = node_param_f32(target, "step_limit", 0.0).max(0.0) * thickness;
 672     if let (Some(before), true) = (before, limit > 0.0) {
 673         for p in (0..n).filter(|&p| movable[p]) {
 674             let d = pos[p] - before[p];
 675             let len = d.length();
 676             if len > limit {
 677                 pos[p] = before[p] + d * (limit / len);
 678                 work.limited += 1;
 679                 moved_at_all = true;
 680             }
 681         }
 682     }
 683 
 684     let looking = Looking { thickness, cell: thickness.max(mean_edge(geom, topo)), edges_too };
 685     let mut near = Near::find(topo, before, &pos, looking);
 686     work.grids += 1;
 687     work.edges_searched += near.sides_looked_at;
 688     // What has folded through its own neighbourhood since the step began.
 689     // A node without the row is one from before it.
 690     let folds_too = before.is_some() && crate::geometry::node_param_bool(target, "fold_contact", false);
 691     let mut folds: Vec<Fold> = Vec::new();
 692     if let (Some(was), true) = (before, folds_too) {
 693         folded(topo, was, &pos, edges_too, 0.0, None, &mut folds);
 694         work.folds = folds.len();
 695     }
 696     let mut push = vec![Vec3::ZERO; n];
 697     let mut weight = vec![0.0f32; n];
 698     let mut found: Vec<Contact> = Vec::new();
 699     // Which points have gone through something this pass.
 700     let mut through = vec![false; n];
 701     for _ in 0..iterations {
 702         work.passes += 1;
 703         if near.is_stale(&pos) {
 704             near = Near::find(topo, before, &pos, looking);
 705             work.grids += 1;
 706             work.edges_searched += near.sides_looked_at;
 707         }
 708         found.clear();
 709         through.iter_mut().for_each(|t| *t = false);
 710         // What folded through is put back as far over its neighbour as it
 711         // began, which is nearer than a thickness: that is what a
 712         // neighbour is.
 713         if let Some(was) = before {
 714             for fold in &folds {
 715                 let (who, then, now) = fold.points(topo, was, &pos);
 716                 if fold.sides {
 717                     let ([e0, e1], [f0, f1]) = ([now[0], now[1]], [now[2], now[3]]);
 718                     if let Some((side, at, height)) = edges_went_through([then[0], then[1]], [then[2], then[3]], [e0, e1], [f0, f1], 0.0) {
 719                         let (s, t) = (at[0].clamp(0.0, 1.0), at[1].clamp(0.0, 1.0));
 720                         let below = ((e0 + (e1 - e0) * s) - (f0 + (f1 - f0) * t)).dot(side);
 721                         found.push(Contact { who, share: [1.0 - s, s, t - 1.0, -t], dir: side, deep: height.min(thickness) - below, through: true, edges: true });
 722                         who.iter().for_each(|&p| through[p] = true);
 723                     }
 724                 } else if let Some((side, height)) = went_through(then[0], [then[1], then[2], then[3]], now[0], [now[1], now[2], now[3]], 0.0) {
 725                     let w = crate::spatial::closest_weights_on_triangle(now[0], now[1], now[2], now[3]);
 726                     let below = (now[0] - now[1]).dot(side);
 727                     found.push(Contact { who, share: [1.0, -w[0], -w[1], -w[2]], dir: side, deep: height.min(thickness) - below, through: true, edges: false });
 728                     through[who[0]] = true;
 729                 }
 730             }
 731         }
 732         let (touching, positions) = (&near.touching, &pos);
 733         let by_point = in_pieces(touching.len(), 4096, |pairs| {
 734             let (pos, mut found) = (positions, Vec::new());
 735             for &[p, t] in &touching[pairs] {
 736                 let p = p as usize;
 737                 let [ia, ib, ic] = topo.tris[t as usize].map(|c| c as usize);
 738                 let (a, b, c) = (pos[ia], pos[ib], pos[ic]);
 739                 let who = [p, ia, ib, ic];
 740                 if let Some((side, _)) = before.and_then(|was| went_through(was[p], [was[ia], was[ib], was[ic]], pos[p], [a, b, c], 0.0)) {
 741                     // `side` is the triangle's normal, turned to the side
 742                     // the point came from; it is below the plane by that
 743                     // much and belongs a thickness above it.
 744                     let w = crate::spatial::closest_weights_on_triangle(pos[p], a, b, c);
 745                     let below = (pos[p] - a).dot(side);
 746                     found.push(Contact { who, share: [1.0, -w[0], -w[1], -w[2]], dir: side, deep: thickness - below, through: true, edges: false });
 747                     continue;
 748                 }
 749                 // What is listed is what was near when it was listed, and
 750                 // most of it is not near enough: a triangle whose box is a
 751                 // thickness away on any axis is further than that.
 752                 let (tlo, thi) = (a.min(b).min(c) - thickness, a.max(b).max(c) + thickness);
 753                 if pos[p].cmplt(tlo).any() || pos[p].cmpgt(thi).any() {
 754                     continue;
 755                 }
 756                 let w = crate::spatial::closest_weights_on_triangle(pos[p], a, b, c);
 757                 let d = pos[p] - (a * w[0] + b * w[1] + c * w[2]);
 758                 let len = d.length();
 759                 if len >= thickness {
 760                     continue;
 761                 }
 762                 let dir = if len < 1e-9 {
 763                     let normal = (b - a).cross(c - a).normalize_or_zero();
 764                     if normal == Vec3::ZERO {
 765                         continue;
 766                     }
 767                     normal
 768                 } else {
 769                     d / len
 770                 };
 771                 found.push(Contact { who, share: [1.0, -w[0], -w[1], -w[2]], dir, deep: thickness - len, through: false, edges: false });
 772             }
 773             found
 774         });
 775         found.extend(by_point.into_iter().flatten());
 776         work.searched += n;
 777         let meeting = &near.meeting;
 778         let by_side = in_pieces(meeting.len(), 4096, |pairs| {
 779             let (pos, mut found) = (positions, Vec::new());
 780             for &[i, j] in &meeting[pairs] {
 781                 let (e, f) = (topo.sides[i as usize], topo.sides[j as usize]);
 782                 let ([e0, e1], [f0, f1]) = (e.map(|p| pos[p as usize]), f.map(|p| pos[p as usize]));
 783                 let who = [e[0] as usize, e[1] as usize, f[0] as usize, f[1] as usize];
 784                 let was = before.map(|was| (e.map(|p| was[p as usize]), f.map(|p| was[p as usize])));
 785                 if let Some((side, at, _)) = was.and_then(|(e_was, f_was)| edges_went_through(e_was, f_was, [e0, e1], [f0, f1], 0.0)) {
 786                     let (s, t) = (at[0].clamp(0.0, 1.0), at[1].clamp(0.0, 1.0));
 787                     let below = ((e0 + (e1 - e0) * s) - (f0 + (f1 - f0) * t)).dot(side);
 788                     found.push(Contact { who, share: [1.0 - s, s, t - 1.0, -t], dir: side, deep: thickness - below, through: true, edges: true });
 789                     continue;
 790                 }
 791                 let (lo, hi) = (e0.min(e1), e0.max(e1));
 792                 let (flo, fhi) = (f0.min(f1) - thickness, f0.max(f1) + thickness);
 793                 if hi.cmplt(flo).any() || lo.cmpgt(fhi).any() {
 794                     continue;
 795                 }
 796                 let (s, t) = nearest_on_segments([e0, e1], [f0, f1]);
 797                 // An end is a point, and the point test has it.
 798                 if !(s > ENDS && s < 1.0 - ENDS && t > ENDS && t < 1.0 - ENDS) {
 799                     continue;
 800                 }
 801                 let d = (e0 + (e1 - e0) * s) - (f0 + (f1 - f0) * t);
 802                 let len = d.length();
 803                 if len >= thickness {
 804                     continue;
 805                 }
 806                 let dir = if len < 1e-9 {
 807                     let across = (e1 - e0).cross(f1 - f0).normalize_or_zero();
 808                     if across == Vec3::ZERO {
 809                         continue;
 810                     }
 811                     across
 812                 } else {
 813                     d / len
 814                 };
 815                 found.push(Contact { who, share: [1.0 - s, s, t - 1.0, -t], dir, deep: thickness - len, through: false, edges: true });
 816             }
 817             found
 818         });
 819         found.extend(by_side.into_iter().flatten());
 820         // What has gone through something, this pass.
 821         for contact in found.iter().filter(|c| c.through) {
 822             let subjects = if contact.edges { &contact.who[..] } else { &contact.who[..1] };
 823             subjects.iter().for_each(|&p| through[p] = true);
 824         }
 825         push.iter_mut().for_each(|v| *v = Vec3::ZERO);
 826         weight.iter_mut().for_each(|w| *w = 0.0);
 827         let mut any = false;
 828         for contact in &found {
 829             let Contact { who, share, dir, deep, .. } = *contact;
 830             // What has gone through something is put back before it is
 831             // parted from anything: whatever is beside the thing it went
 832             // through sees it near, on the wrong side, and would push it on.
 833             let subjects = if contact.edges { &who[..] } else { &who[..1] };
 834             if !contact.through && subjects.iter().any(|&p| through[p]) {
 835                 continue;
 836             }
 837             // Inverse masses of one or none, each by the square of its
 838             // share of the contact.
 839             let give: f32 = (0..4).map(|i| free(who[i]) * share[i] * share[i]).sum();
 840             if give <= 0.0 {
 841                 continue;
 842             }
 843             let step = dir * (deep / give);
 844             for i in 0..4 {
 845                 // Each moves by its share; its say in the average is its
 846                 // share too, so one that is barely part of a contact does
 847                 // not water down another it carries.
 848                 let say = free(who[i]) * share[i].abs() * deep;
 849                 push[who[i]] += step * (share[i] * say);
 850                 weight[who[i]] += say;
 851             }
 852             work.contacts += 1;
 853             work.edge_contacts += contact.edges as usize;
 854             work.crossed += contact.through as usize;
 855             any = true;
 856         }
 857         if !any {
 858             break;
 859         }
 860         for p in 0..n {
 861             if weight[p] > 0.0 {
 862                 pos[p] += push[p] / weight[p];
 863             }
 864         }
 865         moved_at_all = true;
 866     }
 867     // The hold. The passes share a move out and average what they are
 868     // given, and under a push that does not let up they can run out before
 869     // a point is back on its side — and a point left through a triangle is,
 870     // to the next step, a point that began there. So whatever is still
 871     // through goes back to where the step began, and what it is through
 872     // with it: the one arrangement of them known not to cross. It costs
 873     // them the step's movement and nothing else.
 874     if let Some(was) = before {
 875         let mut back: Vec<usize> = Vec::new();
 876         // What the last look put back, and so what this one has to look
 877         // at: a pair none of whose points has moved since it was last
 878         // looked at is as it was. The first look is at everything.
 879         let mut stirred = vec![true; n];
 880         for _ in 0..HOLD_ROUNDS {
 881             if near.is_stale(&pos) {
 882                 near = Near::find(topo, before, &pos, looking);
 883                 work.grids += 1;
 884                 work.edges_searched += near.sides_looked_at;
 885             }
 886             back.clear();
 887             let (touching, meeting, positions, stirred_now) = (&near.touching, &near.meeting, &pos, &stirred);
 888             let by_point = in_pieces(touching.len(), 8192, |pairs| {
 889                 let (pos, stirred, mut back) = (positions, stirred_now, Vec::new());
 890                 for &[p, t] in &touching[pairs] {
 891                     let p = p as usize;
 892                     let [ia, ib, ic] = topo.tris[t as usize].map(|c| c as usize);
 893                     if !(stirred[p] || stirred[ia] || stirred[ib] || stirred[ic]) {
 894                         continue;
 895                     }
 896                     if went_through(was[p], [was[ia], was[ib], was[ic]], pos[p], [pos[ia], pos[ib], pos[ic]], HOLD_MARGIN).is_some() {
 897                         back.extend([p, ia, ib, ic]);
 898                     }
 899                 }
 900                 back
 901             });
 902             let by_side = in_pieces(meeting.len(), 8192, |pairs| {
 903                 let (pos, stirred, mut back) = (positions, stirred_now, Vec::new());
 904                 for &[i, j] in &meeting[pairs] {
 905                     let (e, f) = (topo.sides[i as usize], topo.sides[j as usize]);
 906                     if !e.iter().chain(f.iter()).any(|&p| stirred[p as usize]) {
 907                         continue;
 908                     }
 909                     let (e_now, e_was) = (e.map(|p| pos[p as usize]), e.map(|p| was[p as usize]));
 910                     let (f_now, f_was) = (f.map(|p| pos[p as usize]), f.map(|p| was[p as usize]));
 911                     if edges_went_through(e_was, f_was, e_now, f_now, HOLD_MARGIN).is_some() {
 912                         back.extend(e.iter().chain(f.iter()).map(|&p| p as usize));
 913                     }
 914                 }
 915                 back
 916             });
 917             back.extend(by_point.into_iter().chain(by_side).flatten());
 918             if folds_too {
 919                 folded(topo, was, &pos, edges_too, HOLD_MARGIN, Some(&stirred), &mut folds);
 920                 for fold in &folds {
 921                     back.extend(fold.points(topo, was, &pos).0);
 922                 }
 923             }
 924             stirred.iter_mut().for_each(|s| *s = false);
 925             let mut put = 0;
 926             for &i in &back {
 927                 if movable[i] && pos[i] != was[i] {
 928                     pos[i] = was[i];
 929                     stirred[i] = true;
 930                     put += 1;
 931                 }
 932             }
 933             if put == 0 {
 934                 break;
 935             }
 936             work.held += put;
 937             moved_at_all = true;
 938         }
 939     }
 940     if moved_at_all {
 941         for (p, v) in pos.iter().enumerate() {
 942             geom.set_pos(p, *v);
 943         }
 944     }
 945 }
 946 
 947 /// `run` over `0..count` in pieces, on as many threads as the machine has
 948 /// and the work is worth — no piece smaller than `least` — and what each
 949 /// piece made, in order. The order is what makes this a way of RUNNING the
 950 /// work and not a change to it: put end to end, the pieces are what one
 951 /// thread would have made.
 952 ///
 953 /// Threads and not a pool, since the crate has none: a thread costs tens
 954 /// of microseconds to start, which is what `least` is for.
 955 pub(crate) fn in_pieces<R: Send>(count: usize, least: usize, run: impl Fn(std::ops::Range<usize>) -> R + Sync) -> Vec<R> {
 956     let threads = std::thread::available_parallelism().map_or(1, |n| n.get()).min(count / least.max(1)).max(1);
 957     if threads == 1 {
 958         return vec![run(0..count)];
 959     }
 960     let size = count.div_ceil(threads);
 961     std::thread::scope(|scope| {
 962         let run = &run;
 963         let pieces: Vec<_> = (0..threads).map(|k| scope.spawn(move || run((k * size).min(count)..((k + 1) * size).min(count)))).collect();
 964         pieces.into_iter().map(|piece| piece.join().expect("a piece of the detangle solve panicked")).collect()
 965     })
 966 }
 967 
 968 /// What a search is for.
 969 #[derive(Clone, Copy)]
 970 struct Looking {
 971     thickness: f32,
 972     cell: f32,
 973     edges_too: bool,
 974 }
 975 
 976 /// How much further than a thickness the search looks, as a part of one,
 977 /// so that what it finds is still everything near once the passes have
 978 /// moved the points: until any has moved half of this.
 979 const SLACK: f32 = 0.5;
 980 
 981 /// What is near what: the pairs a pass has to look at, found ONCE and kept
 982 /// while the points stay near where they were when it was.
 983 ///
 984 /// The passes of a solve, and the looks of the hold after them, ask the
 985 /// same question of nearly the same positions, and until 2026-09-29 each
 986 /// answered it from the grid: a gather of cells per point and per edge, a
 987 /// search of the rings per candidate, four or five times over. Measured
 988 /// on the sphere test, that — and not the contacts — was what the Surface
 989 /// method cost. The pairs are found with [`SLACK`] to spare, and found
 990 /// again only when a point has moved half of it.
 991 struct Near {
 992     /// Where the points were when this was found.
 993     filed: Vec<Vec3>,
 994     slack: f32,
 995     /// A point and a triangle none of whose corners is in its rings.
 996     touching: Vec<[u32; 2]>,
 997     /// Two sides neither of whose ends is in the rings of the other's.
 998     meeting: Vec<[u32; 2]>,
 999     /// How many sides were searched for sides near them.
1000     sides_looked_at: usize,
1001 }
1002 
1003 impl Near {
1004     fn is_stale(&self, pos: &[Vec3]) -> bool {
1005         let moved = pos.iter().zip(&self.filed).map(|(p, f)| (*p - *f).length_squared()).fold(0.0f32, f32::max);
1006         moved > (self.slack * 0.5) * (self.slack * 0.5)
1007     }
1008 
1009     fn find(topo: &Topo, before: Option<&[Vec3]>, pos: &[Vec3], looking: Looking) -> Near {
1010         let Looking { thickness, cell, edges_too } = looking;
1011         let n = pos.len();
1012         let slack = thickness * SLACK;
1013         let grid = TriCells::build(pos, &topo.tris, cell);
1014         // How far anything has come since the step began: what a point or
1015         // an edge went through may be that far from where either is now.
1016         let travelled = before.map_or(0.0, |b| pos.iter().zip(b).map(|(p, q)| (*p - *q).length_squared()).fold(0.0f32, f32::max).sqrt());
1017         // Which points are near a triangle with a side that is none of
1018         // their own neighbourhood: what says which edges are worth testing
1019         // against others. Two edges nearer than `close` somewhere along
1020         // them have an end within that and half the edge's length of the
1021         // other edge, which is a side of a triangle — so an edge with
1022         // neither end that near anything is near nothing, and most of a
1023         // mesh is. `close` is the thickness, or as far as two edges that
1024         // have passed through each other this step can have come apart
1025         // since.
1026         let mut near_something = vec![false; n];
1027         let close = thickness.max(2.0 * travelled) + slack;
1028         // Half the longest side at each point.
1029         let mut half = vec![0.0f32; n];
1030         if edges_too {
1031             for e in &topo.sides {
1032                 let [a, b] = e.map(|p| p as usize);
1033                 let len = (pos[b] - pos[a]).length() * 0.5;
1034                 (half[a], half[b]) = (half[a].max(len), half[b].max(len));
1035             }
1036         }
1037         // What may touch is what is within a thickness, and what may have
1038         // gone through since the step began is what is within twice what
1039         // anything has travelled: each has come no further than that from
1040         // where they met. Boxes turn most of a cell away; the distance
1041         // itself turns away most of what is left, which on one sheet is
1042         // the sides a few edges off — near enough for their boxes, and
1043         // further than any thickness.
1044         let reach = thickness.max(2.0 * travelled) + slack;
1045         let boxed = |points: &[u32]| points.iter().fold((Vec3::splat(f32::MAX), Vec3::splat(f32::MIN)), |at, &p| (at.0.min(pos[p as usize]), at.1.max(pos[p as usize])));
1046         let by_point = in_pieces(n, 64, |points| {
1047             let mut search = Search::new(topo);
1048             let (mut touching, mut near_something) = (Vec::new(), Vec::new());
1049             for p in points {
1050                 // How far this point looks: for what it may touch, and
1051                 // further for what its edges may.
1052                 let look = if edges_too { close + half[p] } else { reach };
1053                 let mut near = false;
1054                 search.triangles(&grid, pos[p] - look, pos[p] + look);
1055                 for &t in &search.near {
1056                     let corners = topo.tris[t as usize];
1057                     let (tlo, thi) = boxed(&corners);
1058                     if pos[p].cmplt(tlo - look).any() || pos[p].cmpgt(thi + look).any() {
1059                         continue;
1060                     }
1061                     let [a, b, c] = corners.map(|c| pos[c as usize]);
1062                     let away = (pos[p] - crate::spatial::closest_point_on_triangle(pos[p], a, b, c)).length_squared();
1063                     if away > look * look {
1064                         continue;
1065                     }
1066                     // A triangle with two corners that are none of the
1067                     // point's own has a side its edges may meet; one with
1068                     // three is a triangle it may touch.
1069                     let own = corners.iter().filter(|&&c| topo.excludes(p, c)).count();
1070                     if own > 1 || (own == 1 && !edges_too) {
1071                         continue;
1072                     }
1073                     near = true;
1074                     if own == 0 && away <= reach * reach {
1075                         touching.push([p as u32, t]);
1076                     }
1077                 }
1078                 if near {
1079                     near_something.push(p);
1080                 }
1081             }
1082             (touching, near_something)
1083         });
1084         let mut touching = Vec::new();
1085         for (pairs, near) in by_point {
1086             touching.extend(pairs);
1087             near.into_iter().for_each(|p| near_something[p] = true);
1088         }
1089         let near_something = &near_something;
1090         let mut meeting = Vec::new();
1091         let mut sides_looked_at = 0;
1092         if edges_too {
1093             let by_side = in_pieces(topo.sides.len(), 64, |sides| {
1094                 let mut search = Search::new(topo);
1095                 let (mut meeting, mut looked_at) = (Vec::new(), 0);
1096                 for i in sides {
1097                     let e = topo.sides[i];
1098                     if !e.iter().any(|&p| near_something[p as usize]) {
1099                         continue;
1100                     }
1101                     looked_at += 1;
1102                     let (lo, hi) = boxed(&e);
1103                     let [e0, e1] = e.map(|p| pos[p as usize]);
1104                     search.sides(&grid, topo, i, lo - reach, hi + reach);
1105                     for &j in &search.near {
1106                         let f = topo.sides[j as usize];
1107                         let (flo, fhi) = boxed(&f);
1108                         if hi.cmplt(flo - reach).any() || lo.cmpgt(fhi + reach).any() {
1109                             continue;
1110                         }
1111                         let [f0, f1] = f.map(|p| pos[p as usize]);
1112                         let (s, t) = nearest_on_segments([e0, e1], [f0, f1]);
1113                         if ((e0 + (e1 - e0) * s) - (f0 + (f1 - f0) * t)).length_squared() > reach * reach {
1114                             continue;
1115                         }
1116                         if e.iter().any(|&p| f.iter().any(|&q| topo.excludes(p as usize, q))) {
1117                             continue;
1118                         }
1119                         meeting.push([i as u32, j]);
1120                     }
1121                 }
1122                 (meeting, looked_at)
1123             });
1124             for (pairs, looked_at) in by_side {
1125                 meeting.extend(pairs);
1126                 sides_looked_at += looked_at;
1127             }
1128         }
1129         Near { filed: pos.to_vec(), slack, touching, meeting, sides_looked_at }
1130     }
1131 }
1132 
1133 /// A point and a triangle of its own neighbourhood, or two sides that are
1134 /// each other's, one of which went through the other.
1135 #[derive(Clone, Copy)]
1136 struct Fold {
1137     sides: bool,
1138     /// The point and the triangle, or the two sides.
1139     pair: [u32; 2],
1140 }
1141 
1142 impl Fold {
1143     /// The four points of it, where they were and where they are.
1144     fn points(&self, topo: &Topo, was: &[Vec3], pos: &[Vec3]) -> ([usize; 4], [Vec3; 4], [Vec3; 4]) {
1145         let who = if self.sides {
1146             let ([a, b], [c, d]) = (topo.sides[self.pair[0] as usize], topo.sides[self.pair[1] as usize]);
1147             [a, b, c, d].map(|p| p as usize)
1148         } else {
1149             let [a, b, c] = topo.tris[self.pair[1] as usize];
1150             [self.pair[0], a, b, c].map(|p| p as usize)
1151         };
1152         (who, who.map(|p| was[p]), who.map(|p| pos[p]))
1153     }
1154 }
1155 
1156 /// Everything that has gone through its own NEIGHBOURHOOD since the step
1157 /// began: a point through a triangle with a corner inside the point's
1158 /// rings, a side through a side with an end inside the other's.
1159 ///
1160 /// The rings are excluded from contact because a neighbour is nearer than
1161 /// a thickness by construction, and no distance says whether it is too
1162 /// near. Going THROUGH is not a distance. A point that was on one side of
1163 /// its neighbour's triangle and is on the other has folded the surface
1164 /// through itself, whatever the thickness, and the only pairs with nothing
1165 /// to say are the ones that share a point — which meet there.
1166 ///
1167 /// Found through the mesh and not through the grid: what is in a point's
1168 /// rings is the triangles at the points of its rings, however far apart
1169 /// the fold has left them.
1170 fn folded(topo: &Topo, was: &[Vec3], pos: &[Vec3], sides_too: bool, margin: f32, stirred: Option<&[bool]>, out: &mut Vec<Fold>) {
1171     out.clear();
1172     // With `stirred`, only the pairs with a point that is: what has a
1173     // stirred point in its rings, or is one, is warm, and of a warm
1174     // point's pairs the ones with no stirred point are as they were.
1175     let is = |p: u32| stirred.is_none_or(|s| s[p as usize]);
1176     let warm: Option<Vec<bool>> = stirred.map(|s| (0..pos.len()).map(|p| topo.own(p).iter().any(|&q| topo.neighbours_stirred(q, s))).collect());
1177     let is_warm = |p: u32| warm.as_ref().is_none_or(|w| w[p as usize]);
1178     let by_point = in_pieces(pos.len(), 256, |points| {
1179         let mut search = Search::new(topo);
1180         let mut out = Vec::new();
1181         for p in points {
1182             if !is_warm(p as u32) {
1183                 continue;
1184             }
1185             search.next();
1186             for &q in topo.own(p) {
1187                 for &t in topo.tris_at(q as usize) {
1188                     if search.seen[t as usize] == search.stamp {
1189                         continue;
1190                     }
1191                     search.seen[t as usize] = search.stamp;
1192                     let corners = topo.tris[t as usize];
1193                     if corners.contains(&(p as u32)) || !(is(p as u32) || corners.iter().any(|&c| is(c))) {
1194                         continue;
1195                     }
1196                     let [a, b, c] = corners.map(|c| c as usize);
1197                     if went_through(was[p], [was[a], was[b], was[c]], pos[p], [pos[a], pos[b], pos[c]], margin).is_some() {
1198                         out.push(Fold { sides: false, pair: [p as u32, t] });
1199                     }
1200                 }
1201             }
1202         }
1203         out
1204     });
1205     out.extend(by_point.into_iter().flatten());
1206     if !sides_too {
1207         return;
1208     }
1209     let by_side = in_pieces(topo.sides.len(), 256, |sides| {
1210         let mut search = Search::new(topo);
1211         let mut out = Vec::new();
1212         for i in sides {
1213             let e = topo.sides[i];
1214             if !e.iter().any(|&p| is_warm(p)) {
1215                 continue;
1216             }
1217             search.next();
1218             let (e_was, e_now) = (e.map(|p| was[p as usize]), e.map(|p| pos[p as usize]));
1219             for &end in &e {
1220                 for &q in topo.own(end as usize) {
1221                     for &j in topo.sides_at(q as usize) {
1222                         if j as usize <= i || search.met[j as usize] == search.stamp {
1223                             continue;
1224                         }
1225                         search.met[j as usize] = search.stamp;
1226                         let f = topo.sides[j as usize];
1227                         if f.iter().any(|q| e.contains(q)) || !e.iter().chain(f.iter()).any(|&p| is(p)) {
1228                             continue;
1229                         }
1230                         let (f_was, f_now) = (f.map(|p| was[p as usize]), f.map(|p| pos[p as usize]));
1231                         if edges_went_through(e_was, f_was, e_now, f_now, margin).is_some() {
1232                             out.push(Fold { sides: true, pair: [i as u32, j] });
1233                         }
1234                     }
1235                 }
1236             }
1237         }
1238         out
1239     });
1240     out.extend(by_side.into_iter().flatten());
1241 }
1242 
1243 /// A contact as a pass found it: a point and a triangle's three corners,
1244 /// or two edges' four ends.
1245 #[derive(Clone, Copy)]
1246 struct Contact {
1247     who: [usize; 4],
1248     /// How much of the move each takes, and which way: the point one, the
1249     /// corners against it by how much of the closest point each is; an
1250     /// edge's ends by how near each is to where the edges are nearest, and
1251     /// the other edge's against them.
1252     share: [f32; 4],
1253     /// The way the first of them is to move.
1254     dir: Vec3,
1255     /// How far they are to part.
1256     deep: f32,
1257     through: bool,
1258     edges: bool,
1259 }
1260 
1261 /// How near its end, as a part of its length, the nearest place on an edge
1262 /// may be and still be the edge's and not the end's.
1263 const ENDS: f32 = 1e-3;
1264 
1265 /// What a search of the grid keeps between one query and the next.
1266 struct Search {
1267     near: Vec<u32>,
1268     seen: Vec<u32>,
1269     stamp: u32,
1270     /// One mark per side, as `seen` is one per triangle.
1271     met: Vec<u32>,
1272     /// The triangles a search for sides went through.
1273     held: Vec<u32>,
1274 }
1275 
1276 impl Search {
1277     fn new(topo: &Topo) -> Search {
1278         Search { near: Vec::new(), seen: vec![u32::MAX; topo.tris.len()], stamp: 0, met: vec![u32::MAX; topo.sides.len()], held: Vec::new() }
1279     }
1280 
1281     fn next(&mut self) {
1282         self.stamp = self.stamp.wrapping_add(1);
1283         if self.stamp == u32::MAX {
1284             self.seen.iter_mut().for_each(|m| *m = u32::MAX);
1285             self.met.iter_mut().for_each(|m| *m = u32::MAX);
1286             self.stamp = 0;
1287         }
1288     }
1289 
1290     /// The triangles filed in the cells the box touches, into `near`.
1291     fn triangles(&mut self, grid: &TriCells, lo: Vec3, hi: Vec3) {
1292         self.next();
1293         grid.gather(lo, hi, &mut self.seen, self.stamp, &mut self.near);
1294     }
1295 
1296     /// The sides of those triangles that come AFTER side `i`, each once,
1297     /// into `near`: a pair of sides is met from the earlier of the two.
1298     fn sides(&mut self, grid: &TriCells, topo: &Topo, i: usize, lo: Vec3, hi: Vec3) {
1299         self.triangles(grid, lo, hi);
1300         std::mem::swap(&mut self.near, &mut self.held);
1301         self.near.clear();
1302         for &t in &self.held {
1303             for &j in &topo.tri_sides[t as usize] {
1304                 if j as usize > i && self.met[j as usize] != self.stamp {
1305                     self.met[j as usize] = self.stamp;
1306                     self.near.push(j);
1307                 }
1308             }
1309         }
1310     }
1311 }
1312 
1313 /// Where two segments are nearest each other, as a part of each one's
1314 /// length. Ericson's *Real-Time Collision Detection* §5.1.9.
1315 fn nearest_on_segments([p1, q1]: [Vec3; 2], [p2, q2]: [Vec3; 2]) -> (f32, f32) {
1316     let (d1, d2, r) = (q1 - p1, q2 - p2, p1 - p2);
1317     let (a, e, f) = (d1.length_squared(), d2.length_squared(), d2.dot(r));
1318     if a <= 1e-30 && e <= 1e-30 {
1319         return (0.0, 0.0);
1320     }
1321     if a <= 1e-30 {
1322         return (0.0, (f / e).clamp(0.0, 1.0));
1323     }
1324     let c = d1.dot(r);
1325     if e <= 1e-30 {
1326         return ((-c / a).clamp(0.0, 1.0), 0.0);
1327     }
1328     let b = d1.dot(d2);
1329     let denom = a * e - b * b;
1330     let mut s = if denom > 1e-12 * a * e { ((b * f - c * e) / denom).clamp(0.0, 1.0) } else { 0.0 };
1331     let mut t = (b * s + f) / e;
1332     if t < 0.0 {
1333         t = 0.0;
1334         s = (-c / a).clamp(0.0, 1.0);
1335     } else if t > 1.0 {
1336         t = 1.0;
1337         s = ((b - c) / a).clamp(0.0, 1.0);
1338     }
1339     (s, t)
1340 }
1341 
1342 /// One edge over another, as lines: how far the first is from the second
1343 /// along the direction across both, where on each the two are nearest (as
1344 /// a part of its length, outside nought to one beyond its ends), and that
1345 /// direction. `None` for edges that run the same way, which have none.
1346 fn over_edge([e0, e1]: [Vec3; 2], [f0, f1]: [Vec3; 2]) -> Option<(f32, [f32; 2], Vec3)> {
1347     let (d1, d2, r) = (e1 - e0, f1 - f0, e0 - f0);
1348     let across = d1.cross(d2);
1349     let (a, e) = (d1.length_squared(), d2.length_squared());
1350     let denom = across.length_squared();
1351     if denom <= 1e-8 * a * e {
1352         return None;
1353     }
1354     let (b, c, f) = (d1.dot(d2), d1.dot(r), d2.dot(r));
1355     let s = (b * f - c * e) / denom;
1356     let t = (a * f - b * c) / denom;
1357     let across = across / denom.sqrt();
1358     Some((r.dot(across), [s, t], across))
1359 }
1360 
1361 /// When, between nought and one, something that is `at(0)` on one side of
1362 /// nothing and `at(1)` on the other is nothing: found by halving, since
1363 /// what is asked of is a cubic in the time and its ends are all that is
1364 /// known of it.
1365 fn crossing_time(at: impl Fn(f32) -> f32, began: f32) -> f32 {
1366     let (mut lo, mut hi) = (0.0f32, 1.0f32);
1367     for _ in 0..20 {
1368         let mid = (lo + hi) * 0.5;
1369         if at(mid) * began > 0.0 {
1370             lo = mid;
1371         } else {
1372             hi = mid;
1373         }
1374     }
1375     (lo + hi) * 0.5
1376 }
1377 
1378 /// Whether two edges passed through each other between then and now, and
1379 /// if so the direction across them as they are now, turned to the side the
1380 /// first came from, where on each they are nearest, and how far apart
1381 /// they began.
1382 ///
1383 /// [`went_through`]'s question, of two edges, each point taken to have
1384 /// gone straight from where it was to where it is: the volume the four
1385 /// span changed sign — they were in one plane at some moment between — and
1386 /// at that moment the lines met within both edges. A volume is also
1387 /// nothing when the two run the same way, which is no meeting, and then
1388 /// there is no place on either where they are nearest.
1389 fn edges_went_through(e_then: [Vec3; 2], f_then: [Vec3; 2], e_now: [Vec3; 2], f_now: [Vec3; 2], margin: f32) -> Option<(Vec3, [f32; 2], f32)> {
1390     let volume = |e: [Vec3; 2], f: [Vec3; 2]| (e[0] - f[0]).dot((e[1] - e[0]).cross(f[1] - f[0]));
1391     let (v0, v1) = (volume(e_then, f_then), volume(e_now, f_now));
1392     if v0 == 0.0 || v0 * v1 >= 0.0 {
1393         return None;
1394     }
1395     let between = |when: f32| ([0, 1].map(|i| e_then[i].lerp(e_now[i], when)), [0, 1].map(|i| f_then[i].lerp(f_now[i], when)));
1396     let when = crossing_time(|t| { let (e, f) = between(t); volume(e, f) }, v0);
1397     let (e, f) = between(when);
1398     let (_, at, _) = over_edge(e, f)?;
1399     if !at.iter().all(|&a| a >= -margin && a <= 1.0 + margin) {
1400         return None;
1401     }
1402     let (began, _, _) = over_edge(e_then, f_then)?;
1403     let (_, at_now, across) = over_edge(e_now, f_now)?;
1404     Some((across * v0.signum(), at_now, began.abs()))
1405 }
1406 
1407 /// A point's height over a triangle's plane, along the normal its winding
1408 /// gives, and where its foot is in the triangle's own terms: three weights
1409 /// summing to one, any of them negative outside it. `None` for a triangle
1410 /// with no area.
1411 fn over_triangle(p: Vec3, [a, b, c]: [Vec3; 3]) -> Option<(f32, [f32; 3], Vec3)> {
1412     let (e1, e2, v) = (b - a, c - a, p - a);
1413     let n = e1.cross(e2);
1414     let nn = n.length_squared();
1415     if nn < 1e-30 {
1416         return None;
1417     }
1418     let (w1, w2) = (v.cross(e2).dot(n) / nn, e1.cross(v).dot(n) / nn);
1419     let normal = n / nn.sqrt();
1420     Some((v.dot(normal), [1.0 - w1 - w2, w1, w2], normal))
1421 }
1422 
1423 /// How far outside a triangle, in its own weights, a passage still counts
1424 /// as through it when the question is whether to HOLD the point. A point
1425 /// going through the edge two triangles share is, after rounding, a little
1426 /// outside both, and holding one that did not quite go through costs a
1427 /// step's movement there and nothing else. The passes take no margin: a
1428 /// push is along the triangle's normal, and measured on the sphere test a
1429 /// tenth of a margin pushed points off triangles they had gone around,
1430 /// leaving more crossed than no memory at all.
1431 const HOLD_MARGIN: f32 = 0.05;
1432 
1433 /// How many times the hold looks again: putting points back can leave
1434 /// others through what was put back.
1435 const HOLD_ROUNDS: usize = 8;
1436 
1437 /// Whether a point went through a triangle between then and now, and if so
1438 /// the triangle's normal as it is now, turned to the side the point came
1439 /// from, and how far over the triangle it began.
1440 ///
1441 /// Both move, each point taken to have gone straight from where it was to
1442 /// where it is. It went through if the volume the four span changed sign —
1443 /// the point was in the triangle's plane at some moment between — and at
1444 /// that moment its foot was inside the triangle. Until 2026-09-29 the
1445 /// moment and the foot were read off a straight line between the two ends'
1446 /// heights and weights, which is right for a small step and wrong for a
1447 /// long one: a point carried across several triangles was said to have
1448 /// gone around the one it went through.
1449 fn went_through(p_then: Vec3, tri_then: [Vec3; 3], p_now: Vec3, tri_now: [Vec3; 3], margin: f32) -> Option<(Vec3, f32)> {
1450     let volume = |p: Vec3, [a, b, c]: [Vec3; 3]| (p - a).dot((b - a).cross(c - a));
1451     let (v0, v1) = (volume(p_then, tri_then), volume(p_now, tri_now));
1452     if v0 == 0.0 || v0 * v1 >= 0.0 {
1453         return None;
1454     }
1455     let between = |when: f32| (p_then.lerp(p_now, when), [0, 1, 2].map(|i| tri_then[i].lerp(tri_now[i], when)));
1456     let when = crossing_time(|t| { let (p, tri) = between(t); volume(p, tri) }, v0);
1457     let (p, tri) = between(when);
1458     let (_, w, _) = over_triangle(p, tri)?;
1459     if !w.iter().all(|&w| w >= -margin) {
1460         return None;
1461     }
1462     let (began, _, _) = over_triangle(p_then, tri_then)?;
1463     let (_, _, normal) = over_triangle(p_now, tri_now)?;
1464     Some((normal * v0.signum(), began.abs()))
1465 }
1466 
1467 /// Where a surface passes through itself.
1468 #[derive(Debug, Default, Clone, PartialEq)]
1469 pub struct Tangles {
1470     /// Edge-triangle pairs where the edge passes through the triangle.
1471     pub crossings: usize,
1472     /// The points of those edges and triangles, ascending, each once.
1473     pub points: Vec<u32>,
1474 }
1475 
1476 /// Every edge of `geom` that passes through one of its triangles.
1477 ///
1478 /// This is the MEASURE, and it is geometric: no thickness and no rings. An
1479 /// edge is not tested against a triangle it shares a point with — they meet
1480 /// there by construction — nor against one fanned from a primitive the edge
1481 /// is a side of.
1482 pub fn self_intersections(geom: &Detail) -> Tangles {
1483     if geom.num_points() == 0 || geom.num_prims() == 0 {
1484         return Tangles::default();
1485     }
1486     let topo = topo_for(geom, 0);
1487     if topo.edges.is_empty() {
1488         return Tangles::default();
1489     }
1490     intersections_of(geom, &topo, false)
1491 }
1492 
1493 /// [`self_intersections`] counting only what the solve is MEANT to see at
1494 /// this ring count: an edge through a triangle none of whose corners is
1495 /// within `rings` of either end of it. The rest are folds inside the
1496 /// excluded neighbourhood, which the node leaves alone by design, and
1497 /// telling the two apart is what says whether a crossing is the method's
1498 /// miss or the setting's.
1499 pub fn crossings_beyond(geom: &Detail, rings: usize) -> usize {
1500     if geom.num_points() == 0 || geom.num_prims() == 0 {
1501         return 0;
1502     }
1503     let topo = topo_for(geom, rings);
1504     if topo.edges.is_empty() {
1505         return 0;
1506     }
1507     intersections_of(geom, &topo, true).crossings
1508 }
1509 
1510 fn intersections_of(geom: &Detail, topo: &Topo, beyond_rings: bool) -> Tangles {
1511     let pos: Vec<Vec3> = (0..geom.num_points()).map(|p| geom.pos(p)).collect();
1512     let grid = TriCells::build(&pos, &topo.tris, mean_edge(geom, topo));
1513     let mut found = Tangles::default();
1514     let mut marked = vec![false; pos.len()];
1515     let mut near = Vec::new();
1516     let mut seen = vec![u32::MAX; topo.tris.len()];
1517     for (stamp, e) in topo.edges.iter().enumerate() {
1518         let (a, b) = (pos[e[0] as usize], pos[e[1] as usize]);
1519         grid.gather(a.min(b), a.max(b), &mut seen, stamp as u32, &mut near);
1520         for &t in &near {
1521             let tri = topo.tris[t as usize];
1522             if tri.contains(&e[0]) || tri.contains(&e[1]) {
1523                 continue;
1524             }
1525             if beyond_rings && tri.iter().any(|&c| topo.excludes(e[0] as usize, c) || topo.excludes(e[1] as usize, c)) {
1526                 continue;
1527             }
1528             let of = geom.prim_points(topo.tri_prims[t as usize] as usize);
1529             if of.contains(&e[0]) && of.contains(&e[1]) {
1530                 continue;
1531             }
1532             let [v0, v1, v2] = tri.map(|i| pos[i as usize]);
1533             if crate::spatial::segment_crosses_triangle(a, b, v0, v1, v2) {
1534                 found.crossings += 1;
1535                 for i in e.iter().chain(tri.iter()) {
1536                     marked[*i as usize] = true;
1537                 }
1538             }
1539         }
1540     }
1541     found.points = (0..pos.len() as u32).filter(|&p| marked[p as usize]).collect();
1542     found
1543 }