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

src/detail.rs (72.8K)

   1 //! The geometry container: points, vertices, primitives and detail.
   2 //!
   3 //! This is the Phase 0 replacement for the triangle soup the app started on
   4 //! (a `Vec` of corners, attributes stored per corner), gone since 2026-09-28
   5 //! with the kernel generators that were its last consumer. See
   6 //! `shapeshifter.md` for why: every attribute operator worth having is a
   7 //! statement about a point *and its neighbours*, and a soup has no points, no
   8 //! edges, and no identity that survives a frame.
   9 //!
  10 //! Four element classes, exactly Houdini's:
  11 //!
  12 //! - **Points** carry position and the attributes that describe a place on the
  13 //!   surface. A point is shared by every primitive that uses it — moving one
  14 //!   moves them all, which is the whole difference from a soup.
  15 //! - **Vertices** are a primitive's references to points, in winding order. One
  16 //!   per corner. They exist so a point can carry different per-corner data (a
  17 //!   UV seam, a hard normal) without splitting the point itself.
  18 //! - **Primitives** are runs of vertices. Polygons of any size, not just
  19 //!   triangles; triangulation is a render concern (see [`Detail::triangulate`]).
  20 //! - **Detail** is the single-element class: one row holding whole-geometry
  21 //!   values, which is where `Analysis` writes a range rather than inventing a
  22 //!   dictionary type.
  23 //!
  24 //! Three properties the soup could not have, all load-bearing for later phases:
  25 //!
  26 //! **Columnar attributes.** One array per named attribute, not a `HashMap` per
  27 //! element. This is the layout a GPU buffer already wants, so the Phase 1 kernel
  28 //! ABI binds a slice instead of marshalling a million little maps.
  29 //!
  30 //! **Stable point ids.** [`PointId`] is allocated once per point and preserved by
  31 //! every operator that does not create points. A solver needs to know that the
  32 //! point it is looking at is the one it wrote to last step; a positional weld
  33 //! cannot answer that, because points move.
  34 //!
  35 //! **Real groups.** A named membership set per class, not the `group:<name>`
  36 //! key convention the soup used in its attribute map.
  37 //!
  38 //! Topology (point→prim, point→point, the edge list) is *derived*, built lazily
  39 //! on first ask and dropped on any structural edit — so a chain of ten attribute
  40 //! nodes builds it once rather than welding from scratch in every node, which is
  41 //! what `resolve_relax_geometry_with_errors` has to do today.
  42 
  43 use glam::Vec3;
  44 use std::collections::HashMap;
  45 use std::sync::OnceLock;
  46 
  47 /// A point's identity, stable across the operators that preserve points and
  48 /// across simulation steps. Allocated by [`Detail::add_point`]; never reused
  49 /// within one `Detail`.
  50 pub type PointId = u64;
  51 
  52 /// The conventional color attribute. Read by [`Detail::triangulate`] when
  53 /// present; geometry without it renders at [`DEFAULT_COLOR`].
  54 pub const CD: &str = "Cd";
  55 
  56 /// Point attributes under this prefix are MARKER REQUESTS, not data: the
  57 /// Visualize node copies a vector attribute into `vis_<name>`, already scaled,
  58 /// and the viewport draws a segment from each point along it.
  59 ///
  60 /// A naming convention rather than a side-channel on the container, so the
  61 /// request travels with the geometry through every operator that already knows
  62 /// how to carry an attribute, and shows up in the spreadsheet where you can
  63 /// see what is being drawn and why.
  64 pub const VIS_PREFIX: &str = "vis_";
  65 
  66 /// What a point renders as when it carries no `Cd`.
  67 pub const DEFAULT_COLOR: [f32; 3] = [0.8, 0.8, 0.8];
  68 
  69 /// Which element class an attribute or group belongs to.
  70 #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
  71 pub enum Class {
  72     Point,
  73     Vertex,
  74     Prim,
  75     Detail,
  76 }
  77 
  78 impl Class {
  79     pub fn name(self) -> &'static str {
  80         match self {
  81             Class::Point => "point",
  82             Class::Vertex => "vertex",
  83             Class::Prim => "prim",
  84             Class::Detail => "detail",
  85         }
  86     }
  87 }
  88 
  89 /// An attribute's element type. Integers are here because the Developer set
  90 /// needs counters and ages that stay whole — `Vitality` writes `_age0..2`, and
  91 /// rounding a float age is how off-by-one frames happen.
  92 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
  93 pub enum AttribType {
  94     Float,
  95     Float2,
  96     Float3,
  97     Float4,
  98     Int,
  99 }
 100 
 101 impl AttribType {
 102     /// Component count. `Int` is one component, like `Float`.
 103     pub fn components(self) -> usize {
 104         match self {
 105             AttribType::Float | AttribType::Int => 1,
 106             AttribType::Float2 => 2,
 107             AttribType::Float3 => 3,
 108             AttribType::Float4 => 4,
 109         }
 110     }
 111 
 112     pub fn name(self) -> &'static str {
 113         match self {
 114             AttribType::Float => "float",
 115             AttribType::Float2 => "float2",
 116             AttribType::Float3 => "float3",
 117             AttribType::Float4 => "float4",
 118             AttribType::Int => "int",
 119         }
 120     }
 121 }
 122 
 123 /// One element's value, for the get/set paths that do not care about layout.
 124 #[derive(Clone, Copy, PartialEq, Debug)]
 125 pub enum AttribValue {
 126     Float(f32),
 127     Float2([f32; 2]),
 128     Float3([f32; 3]),
 129     Float4([f32; 4]),
 130     Int(i32),
 131 }
 132 
 133 impl AttribValue {
 134     pub fn ty(self) -> AttribType {
 135         match self {
 136             AttribValue::Float(_) => AttribType::Float,
 137             AttribValue::Float2(_) => AttribType::Float2,
 138             AttribValue::Float3(_) => AttribType::Float3,
 139             AttribValue::Float4(_) => AttribType::Float4,
 140             AttribValue::Int(_) => AttribType::Int,
 141         }
 142     }
 143 
 144     /// The value as a float, for the readers that treat every scalar alike.
 145     /// Wider types yield their first component.
 146     pub fn as_f32(self) -> f32 {
 147         match self {
 148             AttribValue::Float(v) => v,
 149             AttribValue::Float2(v) => v[0],
 150             AttribValue::Float3(v) => v[0],
 151             AttribValue::Float4(v) => v[0],
 152             AttribValue::Int(v) => v as f32,
 153         }
 154     }
 155 
 156     /// The value as a vector, for the readers that treat every vector alike.
 157     /// Scalars broadcast across all three components, which is what a scalar
 158     /// used as a multiplier means.
 159     pub fn as_vec3(self) -> Vec3 {
 160         match self {
 161             AttribValue::Float(v) => Vec3::splat(v),
 162             AttribValue::Float2(v) => Vec3::new(v[0], v[1], 0.0),
 163             AttribValue::Float3(v) => Vec3::from(v),
 164             AttribValue::Float4(v) => Vec3::new(v[0], v[1], v[2]),
 165             AttribValue::Int(v) => Vec3::splat(v as f32),
 166         }
 167     }
 168 }
 169 
 170 /// Whether an attribute survives a simulation step.
 171 ///
 172 /// The distinction `developer.md` draws between **live data**, which "runs
 173 /// like a stream through the simulation", and **derivative data**, "calculated
 174 /// anew every frame based on live data". Houdini has no such notion — there,
 175 /// every attribute simply persists, and a chain that forgets to reset its
 176 /// scratch values accumulates them silently until the sim goes wrong in a way
 177 /// that looks like a physics bug.
 178 ///
 179 /// Making it a property of the ATTRIBUTE rather than a list of names on the
 180 /// solver means the node that creates a value declares its nature at the point
 181 /// of creation, where the author knows the answer, instead of somewhere else
 182 /// that has to be kept in step.
 183 ///
 184 /// [`AttribKind::Live`] is the default, because it is the one whose failure
 185 /// mode is visible: a value that should have been cleared and was not shows up
 186 /// as a drift you can watch, where a value that should have persisted and was
 187 /// cleared just quietly reads zero.
 188 #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
 189 pub enum AttribKind {
 190     /// Carried across the step boundary, by point identity where the geometry
 191     /// was rebuilt underneath it.
 192     #[default]
 193     Live,
 194     /// Zeroed at the start of every step; the chain is expected to rebuild it.
 195     Derivative,
 196 }
 197 
 198 /// One attribute's storage: a single array covering every element of the
 199 /// owning class, in element order.
 200 #[derive(Clone, Debug, PartialEq)]
 201 pub enum AttribData {
 202     Float(Vec<f32>),
 203     Float2(Vec<[f32; 2]>),
 204     Float3(Vec<[f32; 3]>),
 205     Float4(Vec<[f32; 4]>),
 206     Int(Vec<i32>),
 207 }
 208 
 209 impl AttribData {
 210     /// An array of `len` elements, every one the type's zero.
 211     pub fn zeroed(ty: AttribType, len: usize) -> Self {
 212         match ty {
 213             AttribType::Float => AttribData::Float(vec![0.0; len]),
 214             AttribType::Float2 => AttribData::Float2(vec![[0.0; 2]; len]),
 215             AttribType::Float3 => AttribData::Float3(vec![[0.0; 3]; len]),
 216             AttribType::Float4 => AttribData::Float4(vec![[0.0; 4]; len]),
 217             AttribType::Int => AttribData::Int(vec![0; len]),
 218         }
 219     }
 220 
 221     /// An array of `len` elements, every one `value`.
 222     pub fn filled(value: AttribValue, len: usize) -> Self {
 223         match value {
 224             AttribValue::Float(v) => AttribData::Float(vec![v; len]),
 225             AttribValue::Float2(v) => AttribData::Float2(vec![v; len]),
 226             AttribValue::Float3(v) => AttribData::Float3(vec![v; len]),
 227             AttribValue::Float4(v) => AttribData::Float4(vec![v; len]),
 228             AttribValue::Int(v) => AttribData::Int(vec![v; len]),
 229         }
 230     }
 231 
 232     pub fn ty(&self) -> AttribType {
 233         match self {
 234             AttribData::Float(_) => AttribType::Float,
 235             AttribData::Float2(_) => AttribType::Float2,
 236             AttribData::Float3(_) => AttribType::Float3,
 237             AttribData::Float4(_) => AttribType::Float4,
 238             AttribData::Int(_) => AttribType::Int,
 239         }
 240     }
 241 
 242     pub fn len(&self) -> usize {
 243         match self {
 244             AttribData::Float(v) => v.len(),
 245             AttribData::Float2(v) => v.len(),
 246             AttribData::Float3(v) => v.len(),
 247             AttribData::Float4(v) => v.len(),
 248             AttribData::Int(v) => v.len(),
 249         }
 250     }
 251 
 252     pub fn is_empty(&self) -> bool {
 253         self.len() == 0
 254     }
 255 
 256     pub fn get(&self, i: usize) -> Option<AttribValue> {
 257         match self {
 258             AttribData::Float(v) => v.get(i).copied().map(AttribValue::Float),
 259             AttribData::Float2(v) => v.get(i).copied().map(AttribValue::Float2),
 260             AttribData::Float3(v) => v.get(i).copied().map(AttribValue::Float3),
 261             AttribData::Float4(v) => v.get(i).copied().map(AttribValue::Float4),
 262             AttribData::Int(v) => v.get(i).copied().map(AttribValue::Int),
 263         }
 264     }
 265 
 266     /// Write one element. The value's type must match the array's — a caller
 267     /// wanting to change an attribute's type replaces the whole array, so that
 268     /// a half-converted attribute is unrepresentable.
 269     pub fn set(&mut self, i: usize, value: AttribValue) -> Result<(), String> {
 270         macro_rules! put {
 271             ($arr:expr, $v:expr) => {{
 272                 let len = $arr.len();
 273                 let slot = $arr
 274                     .get_mut(i)
 275                     .ok_or_else(|| format!("element {} is out of range ({})", i, len))?;
 276                 *slot = $v;
 277                 Ok(())
 278             }};
 279         }
 280         match (self, value) {
 281             (AttribData::Float(a), AttribValue::Float(v)) => put!(a, v),
 282             (AttribData::Float2(a), AttribValue::Float2(v)) => put!(a, v),
 283             (AttribData::Float3(a), AttribValue::Float3(v)) => put!(a, v),
 284             (AttribData::Float4(a), AttribValue::Float4(v)) => put!(a, v),
 285             (AttribData::Int(a), AttribValue::Int(v)) => put!(a, v),
 286             (data, value) => Err(format!(
 287                 "type mismatch: attribute is {}, value is {}",
 288                 data.ty().name(),
 289                 value.ty().name()
 290             )),
 291         }
 292     }
 293 
 294     /// Append one zero element, keeping the array in step with a class that
 295     /// just grew.
 296     pub fn push_zero(&mut self) {
 297         match self {
 298             AttribData::Float(v) => v.push(0.0),
 299             AttribData::Float2(v) => v.push([0.0; 2]),
 300             AttribData::Float3(v) => v.push([0.0; 3]),
 301             AttribData::Float4(v) => v.push([0.0; 4]),
 302             AttribData::Int(v) => v.push(0),
 303         }
 304     }
 305 
 306     /// Grow or shrink to `len`, zero-filling any new elements.
 307     pub fn resize(&mut self, len: usize) {
 308         match self {
 309             AttribData::Float(v) => v.resize(len, 0.0),
 310             AttribData::Float2(v) => v.resize(len, [0.0; 2]),
 311             AttribData::Float3(v) => v.resize(len, [0.0; 3]),
 312             AttribData::Float4(v) => v.resize(len, [0.0; 4]),
 313             AttribData::Int(v) => v.resize(len, 0),
 314         }
 315     }
 316 
 317     /// A new array holding this one's elements at `idx`, in that order.
 318     ///
 319     /// The single primitive every topology-changing operator needs: deleting
 320     /// elements, reordering them, and duplicating them are all a gather, so
 321     /// there is one place where "what happens to the attributes" is answered.
 322     /// Indices out of range contribute a zero rather than panicking — a caller
 323     /// building an index map should not be able to corrupt memory with an
 324     /// arithmetic slip.
 325     pub fn gather(&self, idx: &[u32]) -> AttribData {
 326         macro_rules! pick {
 327             ($arr:expr, $zero:expr, $wrap:path) => {{
 328                 let mut out = Vec::with_capacity(idx.len());
 329                 for &i in idx {
 330                     out.push($arr.get(i as usize).copied().unwrap_or($zero));
 331                 }
 332                 $wrap(out)
 333             }};
 334         }
 335         match self {
 336             AttribData::Float(a) => pick!(a, 0.0, AttribData::Float),
 337             AttribData::Float2(a) => pick!(a, [0.0; 2], AttribData::Float2),
 338             AttribData::Float3(a) => pick!(a, [0.0; 3], AttribData::Float3),
 339             AttribData::Float4(a) => pick!(a, [0.0; 4], AttribData::Float4),
 340             AttribData::Int(a) => pick!(a, 0, AttribData::Int),
 341         }
 342     }
 343 
 344     /// The raw floats behind the array, for a GPU upload or a bulk read.
 345     /// `Int` has no float view and yields `None`.
 346     pub fn as_f32_slice(&self) -> Option<&[f32]> {
 347         match self {
 348             AttribData::Float(v) => Some(v.as_slice()),
 349             AttribData::Float2(v) => Some(bytemuck::cast_slice(v.as_slice())),
 350             AttribData::Float3(v) => Some(bytemuck::cast_slice(v.as_slice())),
 351             AttribData::Float4(v) => Some(bytemuck::cast_slice(v.as_slice())),
 352             AttribData::Int(_) => None,
 353         }
 354     }
 355 }
 356 
 357 /// Every attribute and group belonging to one element class, plus the element
 358 /// count they are all kept in step with.
 359 #[derive(Clone, Debug, Default, PartialEq)]
 360 pub struct AttribStore {
 361     len: usize,
 362     attribs: HashMap<String, AttribData>,
 363     /// Only the attributes that are NOT the default kind appear here, so an
 364     /// absent entry reads as [`AttribKind::Live`] and nothing has to remember
 365     /// to register an ordinary attribute.
 366     kinds: HashMap<String, AttribKind>,
 367     groups: HashMap<String, Vec<bool>>,
 368 }
 369 
 370 impl AttribStore {
 371     /// Whether the store holds nothing at all: no elements, attributes,
 372     /// kinds or groups — what appending into is the same as being `other`.
 373     fn is_bare(&self) -> bool {
 374         self.len == 0 && self.attribs.is_empty() && self.kinds.is_empty() && self.groups.is_empty()
 375     }
 376 
 377     pub fn with_len(len: usize) -> Self {
 378         Self { len, attribs: HashMap::new(), kinds: HashMap::new(), groups: HashMap::new() }
 379     }
 380 
 381     pub fn len(&self) -> usize {
 382         self.len
 383     }
 384 
 385     pub fn is_empty(&self) -> bool {
 386         self.len == 0
 387     }
 388 
 389     /// Attribute names, sorted, so a spreadsheet's columns do not reshuffle
 390     /// between frames on `HashMap` iteration order.
 391     pub fn names(&self) -> Vec<&str> {
 392         let mut names: Vec<&str> = self.attribs.keys().map(|s| s.as_str()).collect();
 393         names.sort_unstable();
 394         names
 395     }
 396 
 397     /// Group names, sorted, for the same reason.
 398     pub fn group_names(&self) -> Vec<&str> {
 399         let mut names: Vec<&str> = self.groups.keys().map(|s| s.as_str()).collect();
 400         names.sort_unstable();
 401         names
 402     }
 403 
 404     pub fn has(&self, name: &str) -> bool {
 405         self.attribs.contains_key(name)
 406     }
 407 
 408     pub fn get(&self, name: &str) -> Option<&AttribData> {
 409         self.attribs.get(name)
 410     }
 411 
 412     pub fn get_mut(&mut self, name: &str) -> Option<&mut AttribData> {
 413         self.attribs.get_mut(name)
 414     }
 415 
 416     /// Create (or replace) an attribute, every element set to `default`.
 417     ///
 418     /// The attribute is [`AttribKind::Live`]; use [`AttribStore::create_kind`]
 419     /// for one the solver should clear each step. Replacing an attribute
 420     /// replaces its kind too — a name reused for a different purpose is a
 421     /// different attribute.
 422     pub fn create(&mut self, name: &str, default: AttribValue) -> &mut AttribData {
 423         self.create_kind(name, default, AttribKind::Live)
 424     }
 425 
 426     /// Create (or replace) an attribute, declaring whether it survives a step.
 427     pub fn create_kind(
 428         &mut self,
 429         name: &str,
 430         default: AttribValue,
 431         kind: AttribKind,
 432     ) -> &mut AttribData {
 433         self.attribs
 434             .insert(name.to_string(), AttribData::filled(default, self.len));
 435         match kind {
 436             AttribKind::Live => self.kinds.remove(name),
 437             other => self.kinds.insert(name.to_string(), other),
 438         };
 439         self.attribs.get_mut(name).expect("just inserted")
 440     }
 441 
 442     /// Whether an attribute survives a simulation step. An attribute nobody
 443     /// declared is Live.
 444     pub fn kind(&self, name: &str) -> AttribKind {
 445         self.kinds.get(name).copied().unwrap_or_default()
 446     }
 447 
 448     /// Declare an existing attribute's kind without disturbing its values.
 449     pub fn set_kind(&mut self, name: &str, kind: AttribKind) {
 450         if !self.attribs.contains_key(name) {
 451             return;
 452         }
 453         match kind {
 454             AttribKind::Live => self.kinds.remove(name),
 455             other => self.kinds.insert(name.to_string(), other),
 456         };
 457     }
 458 
 459     /// The names of every attribute of one kind, sorted.
 460     pub fn names_of_kind(&self, kind: AttribKind) -> Vec<&str> {
 461         let mut names: Vec<&str> = self
 462             .attribs
 463             .keys()
 464             .filter(|n| self.kind(n) == kind)
 465             .map(|s| s.as_str())
 466             .collect();
 467         names.sort_unstable();
 468         names
 469     }
 470 
 471     /// Zero every Derivative attribute, keeping the columns themselves — the
 472     /// step that follows is expected to rebuild the values, and a reader
 473     /// between the two should find the attribute present and empty rather than
 474     /// missing.
 475     pub fn clear_derivatives(&mut self) {
 476         let names: Vec<String> = self.kinds
 477             .iter()
 478             .filter(|(_, &k)| k == AttribKind::Derivative)
 479             .map(|(n, _)| n.clone())
 480             .collect();
 481         for name in names {
 482             if let Some(data) = self.attribs.get_mut(&name) {
 483                 *data = AttribData::zeroed(data.ty(), self.len);
 484             }
 485         }
 486     }
 487 
 488     /// Create the attribute if it is absent, leaving an existing one — and its
 489     /// values — alone. The read path for an operator that wants to write into
 490     /// an attribute it does not own.
 491     pub fn get_or_create(&mut self, name: &str, default: AttribValue) -> &mut AttribData {
 492         if !self.attribs.contains_key(name) {
 493             self.create(name, default);
 494         }
 495         self.attribs.get_mut(name).expect("present or just created")
 496     }
 497 
 498     pub fn remove(&mut self, name: &str) -> Option<AttribData> {
 499         self.kinds.remove(name);
 500         self.attribs.remove(name)
 501     }
 502 
 503     /// Install a whole array as an attribute, which is how a generator that
 504     /// computed every value in one pass writes them — one move instead of an
 505     /// element-at-a-time walk.
 506     ///
 507     /// A length mismatch is refused rather than padded: an array that does not
 508     /// line up with its class is a caller bug, and silently zero-filling it
 509     /// would put the wrong value on every element after the first mistake.
 510     pub fn insert(&mut self, name: &str, data: AttribData) -> Result<(), String> {
 511         if data.len() != self.len {
 512             return Err(format!(
 513                 "attribute {:?} has {} entries, the class has {}",
 514                 name,
 515                 data.len(),
 516                 self.len
 517             ));
 518         }
 519         self.attribs.insert(name.to_string(), data);
 520         Ok(())
 521     }
 522 
 523     pub fn value(&self, name: &str, i: usize) -> Option<AttribValue> {
 524         self.attribs.get(name).and_then(|a| a.get(i))
 525     }
 526 
 527     pub fn set_value(&mut self, name: &str, i: usize, v: AttribValue) -> Result<(), String> {
 528         self.attribs
 529             .get_mut(name)
 530             .ok_or_else(|| format!("no attribute named {:?}", name))?
 531             .set(i, v)
 532     }
 533 
 534     /// Create an empty group, or empty an existing one.
 535     pub fn create_group(&mut self, name: &str) {
 536         self.groups.insert(name.to_string(), vec![false; self.len]);
 537     }
 538 
 539     pub fn has_group(&self, name: &str) -> bool {
 540         self.groups.contains_key(name)
 541     }
 542 
 543     pub fn remove_group(&mut self, name: &str) -> bool {
 544         self.groups.remove(name).is_some()
 545     }
 546 
 547     /// Put one element in a group, creating the group if needed. Out-of-range
 548     /// indices are ignored.
 549     pub fn add_to_group(&mut self, name: &str, i: usize) {
 550         let len = self.len;
 551         let members = self
 552             .groups
 553             .entry(name.to_string())
 554             .or_insert_with(|| vec![false; len]);
 555         if let Some(slot) = members.get_mut(i) {
 556             *slot = true;
 557         }
 558     }
 559 
 560     /// Put one element in a group or take it out, creating the group if
 561     /// needed. Out-of-range indices are ignored.
 562     pub fn set_in_group(&mut self, name: &str, i: usize, member: bool) {
 563         let len = self.len;
 564         let members = self.groups.entry(name.to_string()).or_insert_with(|| vec![false; len]);
 565         if let Some(slot) = members.get_mut(i) {
 566             *slot = member;
 567         }
 568     }
 569 
 570     /// A group's membership, an element a flag: what [`in_group`](Self::in_group)
 571     /// reads, the group found once — for a reader walking every element.
 572     pub fn group(&self, name: &str) -> Option<&[bool]> {
 573         self.groups.get(name).map(Vec::as_slice)
 574     }
 575 
 576     pub fn in_group(&self, name: &str, i: usize) -> bool {
 577         self.groups.get(name).and_then(|m| m.get(i)).copied().unwrap_or(false)
 578     }
 579 
 580     /// The members of a group, in element order. An absent group has no
 581     /// members — asking about a group nobody created is not an error, because
 582     /// a node's Group parameter is routinely left blank.
 583     pub fn group_members(&self, name: &str) -> Vec<u32> {
 584         match self.groups.get(name) {
 585             Some(m) => m
 586                 .iter()
 587                 .enumerate()
 588                 .filter(|(_, &v)| v)
 589                 .map(|(i, _)| i as u32)
 590                 .collect(),
 591             None => Vec::new(),
 592         }
 593     }
 594 
 595     pub fn group_len(&self, name: &str) -> usize {
 596         self.groups
 597             .get(name)
 598             .map(|m| m.iter().filter(|&&v| v).count())
 599             .unwrap_or(0)
 600     }
 601 
 602     /// Append one element's worth of room to every attribute and group.
 603     fn push_element(&mut self) {
 604         self.len += 1;
 605         for a in self.attribs.values_mut() {
 606             a.push_zero();
 607         }
 608         for g in self.groups.values_mut() {
 609             g.push(false);
 610         }
 611     }
 612 
 613     /// Set the element count, resizing every attribute and group to match.
 614     fn set_len(&mut self, len: usize) {
 615         self.len = len;
 616         for a in self.attribs.values_mut() {
 617             a.resize(len);
 618         }
 619         for g in self.groups.values_mut() {
 620             g.resize(len, false);
 621         }
 622     }
 623 
 624     /// Rebuild the store around a new element order: element `n` of the result
 625     /// is element `idx[n]` of this one. See [`AttribData::gather`].
 626     fn gather(&self, idx: &[u32]) -> AttribStore {
 627         let attribs = self
 628             .attribs
 629             .iter()
 630             .map(|(k, v)| (k.clone(), v.gather(idx)))
 631             .collect();
 632         let groups = self
 633             .groups
 634             .iter()
 635             .map(|(k, v)| {
 636                 let picked = idx
 637                     .iter()
 638                     .map(|&i| v.get(i as usize).copied().unwrap_or(false))
 639                     .collect();
 640                 (k.clone(), picked)
 641             })
 642             .collect();
 643         AttribStore { len: idx.len(), attribs, kinds: self.kinds.clone(), groups }
 644     }
 645 
 646     /// Append `other`'s elements. Attributes present on only one side are
 647     /// created on the other and zero-filled there, so a merge never silently
 648     /// drops a column.
 649     fn append(&mut self, other: &AttribStore) {
 650         let (lhs_len, rhs_len) = (self.len, other.len);
 651 
 652         for (name, rhs) in &other.attribs {
 653             match self.attribs.get_mut(name) {
 654                 Some(lhs) if lhs.ty() == rhs.ty() => append_data(lhs, rhs),
 655                 // A type clash keeps the left side and zero-fills: the
 656                 // alternative is dropping one side's values entirely, and a
 657                 // merge is not the place to decide which side is right.
 658                 Some(lhs) => lhs.resize(lhs_len + rhs_len),
 659                 None => {
 660                     let mut fresh = AttribData::zeroed(rhs.ty(), lhs_len);
 661                     append_data(&mut fresh, rhs);
 662                     self.attribs.insert(name.clone(), fresh);
 663                 }
 664             }
 665         }
 666         for (_, lhs) in self.attribs.iter_mut().filter(|(n, _)| !other.attribs.contains_key(*n)) {
 667             lhs.resize(lhs_len + rhs_len);
 668         }
 669 
 670         // A kind declared on either side sticks: the left side wins a
 671         // disagreement, the same way its values do.
 672         for (name, kind) in &other.kinds {
 673             self.kinds.entry(name.clone()).or_insert(*kind);
 674         }
 675 
 676         for (name, rhs) in &other.groups {
 677             let lhs = self
 678                 .groups
 679                 .entry(name.clone())
 680                 .or_insert_with(|| vec![false; lhs_len]);
 681             lhs.extend_from_slice(rhs);
 682         }
 683         for (_, lhs) in self.groups.iter_mut().filter(|(n, _)| !other.groups.contains_key(*n)) {
 684             lhs.resize(lhs_len + rhs_len, false);
 685         }
 686 
 687         self.len = lhs_len + rhs_len;
 688     }
 689 }
 690 
 691 const DETAIL_MAGIC: &[u8; 8] = b"CCEDTL01";
 692 
 693 fn put_u32(out: &mut Vec<u8>, v: u32) {
 694     out.extend_from_slice(&v.to_le_bytes());
 695 }
 696 
 697 fn put_u64(out: &mut Vec<u8>, v: u64) {
 698     out.extend_from_slice(&v.to_le_bytes());
 699 }
 700 
 701 fn put_str(out: &mut Vec<u8>, s: &str) {
 702     put_u32(out, s.len() as u32);
 703     out.extend_from_slice(s.as_bytes());
 704 }
 705 
 706 /// A bounds-checked cursor over a blob. Every read either yields the bytes it
 707 /// promised or fails; nothing here can index past the buffer.
 708 struct Reader<'a> {
 709     b: &'a [u8],
 710     at: usize,
 711 }
 712 
 713 impl<'a> Reader<'a> {
 714     fn take(&mut self, n: usize) -> Result<&'a [u8], String> {
 715         let end = self.at.checked_add(n).ok_or("length overflow")?;
 716         let slice = self.b.get(self.at..end).ok_or("unexpected end of blob")?;
 717         self.at = end;
 718         Ok(slice)
 719     }
 720     fn u32(&mut self) -> Result<u32, String> {
 721         Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
 722     }
 723     fn u64(&mut self) -> Result<u64, String> {
 724         Ok(u64::from_le_bytes(self.take(8)?.try_into().unwrap()))
 725     }
 726     fn f32(&mut self) -> Result<f32, String> {
 727         Ok(f32::from_le_bytes(self.take(4)?.try_into().unwrap()))
 728     }
 729     fn i32(&mut self) -> Result<i32, String> {
 730         Ok(i32::from_le_bytes(self.take(4)?.try_into().unwrap()))
 731     }
 732     fn str(&mut self) -> Result<String, String> {
 733         let n = self.u32()? as usize;
 734         let bytes = self.take(n)?;
 735         String::from_utf8(bytes.to_vec()).map_err(|_| "attribute name is not UTF-8".to_string())
 736     }
 737 }
 738 
 739 impl AttribStore {
 740     fn write_into(&self, out: &mut Vec<u8>) {
 741         put_u32(out, self.len as u32);
 742         let names = self.names();
 743         put_u32(out, names.len() as u32);
 744         for name in names {
 745             let data = &self.attribs[name];
 746             put_str(out, name);
 747             out.push(match data.ty() {
 748                 AttribType::Float => 0,
 749                 AttribType::Float2 => 1,
 750                 AttribType::Float3 => 2,
 751                 AttribType::Float4 => 3,
 752                 AttribType::Int => 4,
 753             });
 754             out.push(match self.kind(name) {
 755                 AttribKind::Live => 0,
 756                 AttribKind::Derivative => 1,
 757             });
 758             match data {
 759                 AttribData::Float(v) => out.extend(v.iter().flat_map(|x| x.to_le_bytes())),
 760                 AttribData::Float2(v) => {
 761                     out.extend(v.iter().flatten().flat_map(|x| x.to_le_bytes()))
 762                 }
 763                 AttribData::Float3(v) => {
 764                     out.extend(v.iter().flatten().flat_map(|x| x.to_le_bytes()))
 765                 }
 766                 AttribData::Float4(v) => {
 767                     out.extend(v.iter().flatten().flat_map(|x| x.to_le_bytes()))
 768                 }
 769                 AttribData::Int(v) => out.extend(v.iter().flat_map(|x| x.to_le_bytes())),
 770             }
 771         }
 772         let groups = self.group_names();
 773         put_u32(out, groups.len() as u32);
 774         for name in groups {
 775             put_str(out, name);
 776             out.extend(self.groups[name].iter().map(|&m| m as u8));
 777         }
 778     }
 779 
 780     fn read_from(r: &mut Reader) -> Result<AttribStore, String> {
 781         let len = r.u32()? as usize;
 782         let mut store = AttribStore::with_len(len);
 783         let n_attrs = r.u32()? as usize;
 784         for _ in 0..n_attrs {
 785             let name = r.str()?;
 786             let ty = match r.take(1)?[0] {
 787                 0 => AttribType::Float,
 788                 1 => AttribType::Float2,
 789                 2 => AttribType::Float3,
 790                 3 => AttribType::Float4,
 791                 4 => AttribType::Int,
 792                 other => return Err(format!("unknown attribute type {other}")),
 793             };
 794             let kind = match r.take(1)?[0] {
 795                 0 => AttribKind::Live,
 796                 1 => AttribKind::Derivative,
 797                 other => return Err(format!("unknown attribute kind {other}")),
 798             };
 799             let data = match ty {
 800                 AttribType::Int => {
 801                     let mut v = Vec::with_capacity(len.min(1 << 20));
 802                     for _ in 0..len {
 803                         v.push(r.i32()?);
 804                     }
 805                     AttribData::Int(v)
 806                 }
 807                 _ => {
 808                     let k = ty.components();
 809                     let mut flat = Vec::with_capacity((len * k).min(1 << 22));
 810                     for _ in 0..len * k {
 811                         flat.push(r.f32()?);
 812                     }
 813                     match ty {
 814                         AttribType::Float => AttribData::Float(flat),
 815                         AttribType::Float2 => {
 816                             AttribData::Float2(flat.chunks_exact(2).map(|c| [c[0], c[1]]).collect())
 817                         }
 818                         AttribType::Float3 => AttribData::Float3(
 819                             flat.chunks_exact(3).map(|c| [c[0], c[1], c[2]]).collect(),
 820                         ),
 821                         _ => AttribData::Float4(
 822                             flat.chunks_exact(4).map(|c| [c[0], c[1], c[2], c[3]]).collect(),
 823                         ),
 824                     }
 825                 }
 826             };
 827             store.attribs.insert(name.clone(), data);
 828             if kind != AttribKind::Live {
 829                 store.kinds.insert(name, kind);
 830             }
 831         }
 832         let n_groups = r.u32()? as usize;
 833         for _ in 0..n_groups {
 834             let name = r.str()?;
 835             let bits = r.take(len)?;
 836             store.groups.insert(name, bits.iter().map(|&b| b != 0).collect());
 837         }
 838         Ok(store)
 839     }
 840 }
 841 
 842 fn append_data(lhs: &mut AttribData, rhs: &AttribData) {
 843     match (lhs, rhs) {
 844         (AttribData::Float(a), AttribData::Float(b)) => a.extend_from_slice(b),
 845         (AttribData::Float2(a), AttribData::Float2(b)) => a.extend_from_slice(b),
 846         (AttribData::Float3(a), AttribData::Float3(b)) => a.extend_from_slice(b),
 847         (AttribData::Float4(a), AttribData::Float4(b)) => a.extend_from_slice(b),
 848         (AttribData::Int(a), AttribData::Int(b)) => a.extend_from_slice(b),
 849         (lhs, rhs) => lhs.resize(lhs.len() + rhs.len()),
 850     }
 851 }
 852 
 853 /// Derived connectivity: which primitives use a point, which points share an
 854 /// edge with it, and the unique edge list.
 855 ///
 856 /// Built on demand by [`Detail::topology`] and dropped by any structural edit.
 857 /// Everything is CSR — a start-offset array indexed by point, plus one flat
 858 /// array of contents — so a neighbour walk is a slice, not an allocation, and
 859 /// the whole thing uploads to a GPU buffer unchanged in Phase 1.
 860 #[derive(Clone, Debug, Default)]
 861 pub struct Topology {
 862     point_prim_start: Vec<u32>,
 863     point_prim: Vec<u32>,
 864     point_nbr_start: Vec<u32>,
 865     point_nbr: Vec<u32>,
 866     edges: Vec<[u32; 2]>,
 867 }
 868 
 869 impl Topology {
 870     /// The primitives using point `p`, ascending.
 871     pub fn point_prims(&self, p: usize) -> &[u32] {
 872         Self::span(&self.point_prim_start, &self.point_prim, p)
 873     }
 874 
 875     /// The points sharing an edge with point `p`, ascending and deduplicated.
 876     pub fn point_neighbours(&self, p: usize) -> &[u32] {
 877         Self::span(&self.point_nbr_start, &self.point_nbr, p)
 878     }
 879 
 880     /// Every unique undirected edge, each as `[low, high]`.
 881     pub fn edges(&self) -> &[[u32; 2]] {
 882         &self.edges
 883     }
 884 
 885     /// How many edges meet at point `p` — Houdini's valence, and the number
 886     /// incremental remeshing steers toward 6.
 887     pub fn valence(&self, p: usize) -> usize {
 888         self.point_neighbours(p).len()
 889     }
 890 
 891     fn span<'a>(start: &[u32], flat: &'a [u32], i: usize) -> &'a [u32] {
 892         if i + 1 >= start.len() {
 893             return &[];
 894         }
 895         let (a, b) = (start[i] as usize, start[i + 1] as usize);
 896         flat.get(a..b).unwrap_or(&[])
 897     }
 898 
 899     fn build(num_points: usize, vert_point: &[u32], prim_start: &[u32]) -> Topology {
 900         let num_prims = prim_start.len().saturating_sub(1);
 901 
 902         // point -> prims, by counting sort: one pass to count, a prefix sum,
 903         // then one pass to place. A point appearing twice in one primitive
 904         // (a degenerate fan) is counted once.
 905         let mut counts = vec![0u32; num_points + 1];
 906         let mut seen: Vec<u32> = Vec::new();
 907         for prim in 0..num_prims {
 908             seen.clear();
 909             for &pt in &vert_point[prim_start[prim] as usize..prim_start[prim + 1] as usize] {
 910                 if !seen.contains(&pt) {
 911                     seen.push(pt);
 912                     if (pt as usize) < num_points {
 913                         counts[pt as usize] += 1;
 914                     }
 915                 }
 916             }
 917         }
 918         let mut point_prim_start = vec![0u32; num_points + 1];
 919         let mut acc = 0u32;
 920         for p in 0..num_points {
 921             point_prim_start[p] = acc;
 922             acc += counts[p];
 923         }
 924         point_prim_start[num_points] = acc;
 925 
 926         let mut cursor = point_prim_start.clone();
 927         let mut point_prim = vec![0u32; acc as usize];
 928         for prim in 0..num_prims {
 929             seen.clear();
 930             for &pt in &vert_point[prim_start[prim] as usize..prim_start[prim + 1] as usize] {
 931                 if !seen.contains(&pt) {
 932                     seen.push(pt);
 933                     if (pt as usize) < num_points {
 934                         point_prim[cursor[pt as usize] as usize] = prim as u32;
 935                         cursor[pt as usize] += 1;
 936                     }
 937                 }
 938             }
 939         }
 940 
 941         let edges = unique_edges(num_points, vert_point, prim_start);
 942 
 943         // point -> neighbours, from the deduplicated edge list. Both endpoints
 944         // of every edge, counting-sorted the same way.
 945         let mut counts = vec![0u32; num_points];
 946         for e in &edges {
 947             for &p in e {
 948                 if (p as usize) < num_points {
 949                     counts[p as usize] += 1;
 950                 }
 951             }
 952         }
 953         let mut point_nbr_start = vec![0u32; num_points + 1];
 954         let mut acc = 0u32;
 955         for p in 0..num_points {
 956             point_nbr_start[p] = acc;
 957             acc += counts[p];
 958         }
 959         point_nbr_start[num_points] = acc;
 960 
 961         let mut cursor = point_nbr_start.clone();
 962         let mut point_nbr = vec![0u32; acc as usize];
 963         for e in &edges {
 964             let (a, b) = (e[0], e[1]);
 965             if (a as usize) < num_points {
 966                 point_nbr[cursor[a as usize] as usize] = b;
 967                 cursor[a as usize] += 1;
 968             }
 969             if (b as usize) < num_points {
 970                 point_nbr[cursor[b as usize] as usize] = a;
 971                 cursor[b as usize] += 1;
 972             }
 973         }
 974         for p in 0..num_points {
 975             let (a, b) = (point_nbr_start[p] as usize, point_nbr_start[p + 1] as usize);
 976             point_nbr[a..b].sort_unstable();
 977         }
 978 
 979         Topology { point_prim_start, point_prim, point_nbr_start, point_nbr, edges }
 980     }
 981 }
 982 
 983 /// The mesh's edges, each once as `[low, high]`, in ascending order: every
 984 /// consecutive pair around each primitive, closing the loop. A two-point
 985 /// primitive (an open line segment) contributes one edge, not two — closing
 986 /// it would invent a neighbour.
 987 ///
 988 /// Sorted by bucket rather than as one list: the edges are counted into
 989 /// their low point's bucket and each bucket, a handful long, is sorted and
 990 /// deduplicated on its own — the order a sort of the whole list gives, and
 991 /// the same list (`unique_edges_match_a_sort_of_every_edge`). Until
 992 /// 2026-10-07 it was that whole sort, two thirds of building a 57k-point
 993 /// mesh's topology, and a playing simulation's wireframe built one every
 994 /// frame. A primitive naming a point past the end (a hand-built mesh) falls
 995 /// back to the whole sort.
 996 fn unique_edges(num_points: usize, vert_point: &[u32], prim_start: &[u32]) -> Vec<[u32; 2]> {
 997     let num_prims = prim_start.len().saturating_sub(1);
 998     let mut all: Vec<[u32; 2]> = Vec::with_capacity(vert_point.len());
 999     for prim in 0..num_prims {
1000         let pts = &vert_point[prim_start[prim] as usize..prim_start[prim + 1] as usize];
1001         let n = pts.len();
1002         if n < 2 {
1003             continue;
1004         }
1005         let span = if n == 2 { 1 } else { n };
1006         for i in 0..span {
1007             let (a, b) = (pts[i], pts[(i + 1) % n]);
1008             if a == b {
1009                 continue;
1010             }
1011             all.push([a.min(b), a.max(b)]);
1012         }
1013     }
1014     if all.iter().any(|e| e[0] as usize >= num_points) {
1015         all.sort_unstable();
1016         all.dedup();
1017         return all;
1018     }
1019     let mut start = vec![0u32; num_points + 1];
1020     for e in &all {
1021         start[e[0] as usize + 1] += 1;
1022     }
1023     for p in 0..num_points {
1024         start[p + 1] += start[p];
1025     }
1026     let mut cursor = start.clone();
1027     let mut highs = vec![0u32; all.len()];
1028     for e in &all {
1029         let at = &mut cursor[e[0] as usize];
1030         highs[*at as usize] = e[1];
1031         *at += 1;
1032     }
1033     let mut edges = Vec::with_capacity(all.len() / 2 + 1);
1034     for a in 0..num_points {
1035         let bucket = &mut highs[start[a] as usize..start[a + 1] as usize];
1036         bucket.sort_unstable();
1037         let mut last = None;
1038         for &b in bucket.iter() {
1039             if last != Some(b) {
1040                 edges.push([a as u32, b]);
1041                 last = Some(b);
1042             }
1043         }
1044     }
1045     edges
1046 }
1047 
1048 /// Points, vertices, primitives and detail — one piece of geometry.
1049 ///
1050 /// See the module docs. Position and [`PointId`] get dedicated fields rather
1051 /// than living in the point attribute store: every operator touches both, and
1052 /// neither should cost a name lookup or be removable.
1053 #[derive(Debug)]
1054 pub struct Detail {
1055     pos: Vec<[f32; 3]>,
1056     ids: Vec<PointId>,
1057     next_id: PointId,
1058     points: AttribStore,
1059     /// One entry per vertex: the point it references. Primitives index into
1060     /// this array through `prim_start`.
1061     vert_point: Vec<u32>,
1062     verts: AttribStore,
1063     /// CSR offsets into `vert_point`, one per primitive plus a trailing total.
1064     /// Always non-empty: a geometry with no primitives still has `[0]`.
1065     prim_start: Vec<u32>,
1066     prims: AttribStore,
1067     detail: AttribStore,
1068     /// Shared by a clone (`Arc`): see [`Clone for Detail`](#impl-Clone-for-Detail).
1069     topo: OnceLock<std::sync::Arc<Topology>>,
1070 }
1071 
1072 impl Default for Detail {
1073     fn default() -> Self {
1074         Self::new()
1075     }
1076 }
1077 
1078 /// Two details are equal when everything a reader can see of them is: the
1079 /// points and their identities, the counter new points draw from, the
1080 /// primitives, and every attribute and group. The derived topology is not
1081 /// compared, being derived.
1082 impl PartialEq for Detail {
1083     fn eq(&self, other: &Self) -> bool {
1084         self.pos == other.pos
1085             && self.ids == other.ids
1086             && self.next_id == other.next_id
1087             && self.vert_point == other.vert_point
1088             && self.prim_start == other.prim_start
1089             && self.points == other.points
1090             && self.verts == other.verts
1091             && self.prims == other.prims
1092             && self.detail == other.detail
1093     }
1094 }
1095 
1096 impl Clone for Detail {
1097     /// The topology, when built, is SHARED with the clone (since
1098     /// 2026-10-07; until then it was deliberately dropped). It is derived
1099     /// from the primitives and the point count alone, every edit of either
1100     /// drops it (`invalidate`, called by every structural writer: `add_point`,
1101     /// `add_points`, `add_prim`, the gathers, `fuse_points`, `merge`), and
1102     /// moving points or writing attributes does not touch it — so a clone
1103     /// edited structurally builds its own, and one that is not keeps a
1104     /// topology that is still true. Dropping it cost every copy of a cached
1105     /// simulation state its whole topology again, a frame at a time:
1106     /// 6 ms at 57k points for the wireframe's edges alone.
1107     fn clone(&self) -> Self {
1108         Self {
1109             pos: self.pos.clone(),
1110             ids: self.ids.clone(),
1111             next_id: self.next_id,
1112             points: self.points.clone(),
1113             vert_point: self.vert_point.clone(),
1114             verts: self.verts.clone(),
1115             prim_start: self.prim_start.clone(),
1116             prims: self.prims.clone(),
1117             detail: self.detail.clone(),
1118             topo: match self.topo.get() {
1119                 Some(t) => OnceLock::from(t.clone()),
1120                 None => OnceLock::new(),
1121             },
1122         }
1123     }
1124 }
1125 
1126 impl Detail {
1127     pub fn new() -> Self {
1128         Self {
1129             pos: Vec::new(),
1130             ids: Vec::new(),
1131             next_id: 0,
1132             points: AttribStore::default(),
1133             vert_point: Vec::new(),
1134             verts: AttribStore::default(),
1135             prim_start: vec![0],
1136             prims: AttribStore::default(),
1137             detail: AttribStore::with_len(1),
1138             topo: OnceLock::new(),
1139         }
1140     }
1141 
1142     // ---- counts ----
1143 
1144     pub fn num_points(&self) -> usize {
1145         self.pos.len()
1146     }
1147 
1148     pub fn num_verts(&self) -> usize {
1149         self.vert_point.len()
1150     }
1151 
1152     pub fn num_prims(&self) -> usize {
1153         self.prim_start.len() - 1
1154     }
1155 
1156     pub fn is_empty(&self) -> bool {
1157         self.pos.is_empty()
1158     }
1159 
1160     // ---- attribute stores ----
1161 
1162     pub fn points(&self) -> &AttribStore {
1163         &self.points
1164     }
1165 
1166     pub fn points_mut(&mut self) -> &mut AttribStore {
1167         &mut self.points
1168     }
1169 
1170     pub fn verts(&self) -> &AttribStore {
1171         &self.verts
1172     }
1173 
1174     pub fn verts_mut(&mut self) -> &mut AttribStore {
1175         &mut self.verts
1176     }
1177 
1178     pub fn prims(&self) -> &AttribStore {
1179         &self.prims
1180     }
1181 
1182     pub fn prims_mut(&mut self) -> &mut AttribStore {
1183         &mut self.prims
1184     }
1185 
1186     /// The single-row detail store — where whole-geometry values live.
1187     pub fn detail(&self) -> &AttribStore {
1188         &self.detail
1189     }
1190 
1191     pub fn detail_mut(&mut self) -> &mut AttribStore {
1192         &mut self.detail
1193     }
1194 
1195     pub fn store(&self, class: Class) -> &AttribStore {
1196         match class {
1197             Class::Point => &self.points,
1198             Class::Vertex => &self.verts,
1199             Class::Prim => &self.prims,
1200             Class::Detail => &self.detail,
1201         }
1202     }
1203 
1204     pub fn store_mut(&mut self, class: Class) -> &mut AttribStore {
1205         match class {
1206             Class::Point => &mut self.points,
1207             Class::Vertex => &mut self.verts,
1208             Class::Prim => &mut self.prims,
1209             Class::Detail => &mut self.detail,
1210         }
1211     }
1212 
1213     // ---- points ----
1214 
1215     pub fn pos(&self, p: usize) -> Vec3 {
1216         self.pos.get(p).map(|v| Vec3::from(*v)).unwrap_or(Vec3::ZERO)
1217     }
1218 
1219     pub fn set_pos(&mut self, p: usize, v: Vec3) {
1220         if let Some(slot) = self.pos.get_mut(p) {
1221             *slot = v.to_array();
1222         }
1223     }
1224 
1225     /// Every position, flat — the slice a GPU buffer takes directly.
1226     pub fn positions(&self) -> &[[f32; 3]] {
1227         &self.pos
1228     }
1229 
1230     /// Positions for in-place editing. Moving points does not change topology,
1231     /// so the cache survives; a caller that adds or removes points must go
1232     /// through [`Detail::add_point`] or [`Detail::gather_points`] instead.
1233     pub fn positions_mut(&mut self) -> &mut [[f32; 3]] {
1234         &mut self.pos
1235     }
1236 
1237     /// The stable identity of point `p`.
1238     pub fn id(&self, p: usize) -> Option<PointId> {
1239         self.ids.get(p).copied()
1240     }
1241 
1242     pub fn ids(&self) -> &[PointId] {
1243         &self.ids
1244     }
1245 
1246     /// The identity the next new point will get.
1247     pub fn next_id(&self) -> PointId {
1248         self.next_id
1249     }
1250 
1251     /// Where the point carrying `id` currently sits, or `None` if it is gone.
1252     /// Linear; a solver resolving many ids at once should build a map with
1253     /// [`Detail::id_map`] instead.
1254     pub fn index_of_id(&self, id: PointId) -> Option<usize> {
1255         self.ids.iter().position(|&i| i == id)
1256     }
1257 
1258     /// Identity to index, for the solver path that reconciles two frames.
1259     pub fn id_map(&self) -> HashMap<PointId, u32> {
1260         self.ids
1261             .iter()
1262             .enumerate()
1263             .map(|(i, &id)| (id, i as u32))
1264             .collect()
1265     }
1266 
1267     /// Replace every point's identity, and the counter new points draw from.
1268     ///
1269     /// For a rebuild that KNOWS which points it preserved — the remesher,
1270     /// which tears a mesh apart and puts it back, and needs the survivors to
1271     /// come out as themselves. Everything else must let [`Detail::add_point`]
1272     /// allocate, or two points end up answering to one identity.
1273     ///
1274     /// A mismatched length is refused rather than padded: a partial identity
1275     /// map is worse than none, because the points it does map look right.
1276     pub fn set_ids(&mut self, ids: Vec<PointId>, next_id: PointId) -> Result<(), String> {
1277         if ids.len() != self.pos.len() {
1278             return Err(format!(
1279                 "{} identities for {} points",
1280                 ids.len(),
1281                 self.pos.len()
1282             ));
1283         }
1284         self.next_id = next_id.max(ids.iter().copied().max().map(|m| m + 1).unwrap_or(0));
1285         self.ids = ids;
1286         Ok(())
1287     }
1288 
1289     /// Add a point at `pos`, assigning it a fresh identity. Returns its index.
1290     pub fn add_point(&mut self, pos: Vec3) -> u32 {
1291         let idx = self.pos.len() as u32;
1292         self.pos.push(pos.to_array());
1293         self.ids.push(self.next_id);
1294         self.next_id += 1;
1295         self.points.push_element();
1296         self.invalidate();
1297         idx
1298     }
1299 
1300     /// Add `n` points at once, which is what a generator does. Returns the
1301     /// index of the first.
1302     pub fn add_points(&mut self, positions: &[[f32; 3]]) -> u32 {
1303         let first = self.pos.len() as u32;
1304         self.pos.extend_from_slice(positions);
1305         for _ in 0..positions.len() {
1306             self.ids.push(self.next_id);
1307             self.next_id += 1;
1308         }
1309         self.points.set_len(self.pos.len());
1310         self.invalidate();
1311         first
1312     }
1313 
1314     // ---- primitives ----
1315 
1316     /// Add a primitive over the given points, in winding order. One vertex per
1317     /// entry. Returns the primitive index.
1318     pub fn add_prim(&mut self, points: &[u32]) -> u32 {
1319         let idx = self.num_prims() as u32;
1320         self.vert_point.extend_from_slice(points);
1321         self.prim_start.push(self.vert_point.len() as u32);
1322         self.verts.set_len(self.vert_point.len());
1323         self.prims.push_element();
1324         self.invalidate();
1325         idx
1326     }
1327 
1328     /// The vertex indices of primitive `p`.
1329     pub fn prim_verts(&self, p: usize) -> std::ops::Range<usize> {
1330         if p + 1 >= self.prim_start.len() {
1331             return 0..0;
1332         }
1333         self.prim_start[p] as usize..self.prim_start[p + 1] as usize
1334     }
1335 
1336     /// The points of primitive `p`, in winding order.
1337     pub fn prim_points(&self, p: usize) -> &[u32] {
1338         let r = self.prim_verts(p);
1339         self.vert_point.get(r).unwrap_or(&[])
1340     }
1341 
1342     /// The point a vertex references.
1343     pub fn vert_point(&self, v: usize) -> Option<u32> {
1344         self.vert_point.get(v).copied()
1345     }
1346 
1347     pub fn vert_points(&self) -> &[u32] {
1348         &self.vert_point
1349     }
1350 
1351     // ---- topology ----
1352 
1353     /// Connectivity, built on first ask and reused until a structural edit
1354     /// drops it.
1355     pub fn topology(&self) -> &Topology {
1356         self.topo
1357             .get_or_init(|| std::sync::Arc::new(Topology::build(self.num_points(), &self.vert_point, &self.prim_start)))
1358     }
1359 
1360     /// About what the topology holds when it is built, in bytes; 0 when it
1361     /// is not. For the simulation checkpoints' budget.
1362     pub fn topology_bytes(&self) -> usize {
1363         self.topo.get().map_or(0, |t| {
1364             4 * (t.point_prim_start.len() + t.point_prim.len() + t.point_nbr_start.len() + t.point_nbr.len())
1365                 + 8 * t.edges.len()
1366         })
1367     }
1368 
1369     /// The points sharing an edge with point `p`.
1370     pub fn point_neighbours(&self, p: usize) -> &[u32] {
1371         self.topology().point_neighbours(p)
1372     }
1373 
1374     /// The primitives using point `p`.
1375     pub fn point_prims(&self, p: usize) -> &[u32] {
1376         self.topology().point_prims(p)
1377     }
1378 
1379     /// Every unique undirected edge.
1380     pub fn edges(&self) -> &[[u32; 2]] {
1381         self.topology().edges()
1382     }
1383 
1384     /// The edges, as [`Detail::edges`] lists them, without building the
1385     /// rest of the topology when it is not already built: what the wire
1386     /// pass needs, every frame of a playing simulation, of a scene nothing
1387     /// else asks the topology of.
1388     pub fn edge_list(&self) -> std::borrow::Cow<'_, [[u32; 2]]> {
1389         match self.topo.get() {
1390             Some(topo) => std::borrow::Cow::Borrowed(topo.edges()),
1391             None => std::borrow::Cow::Owned(unique_edges(self.num_points(), &self.vert_point, &self.prim_start)),
1392         }
1393     }
1394 
1395     /// Every point's colour as [`Detail::color`] reads it, the column found
1396     /// once — for the readers that walk every point or corner, where a
1397     /// lookup by name each time was most of what they cost.
1398     pub fn point_colors(&self) -> std::borrow::Cow<'_, [[f32; 3]]> {
1399         let n = self.num_points();
1400         match self.points.get(CD) {
1401             Some(AttribData::Float3(c)) => std::borrow::Cow::Borrowed(c),
1402             Some(other) => std::borrow::Cow::Owned(
1403                 (0..n).map(|p| other.get(p).map_or(DEFAULT_COLOR, |v| v.as_vec3().to_array())).collect(),
1404             ),
1405             None => std::borrow::Cow::Owned(vec![DEFAULT_COLOR; n]),
1406         }
1407     }
1408 
1409     /// Drop the derived topology. Called by every structural edit; public
1410     /// because an operator writing `vert_point` through a future bulk path
1411     /// must be able to say so.
1412     /// Whether the topology is built (and so free to ask for).
1413     pub fn has_topology(&self) -> bool {
1414         self.topo.get().is_some()
1415     }
1416 
1417     pub fn invalidate(&mut self) {
1418         self.topo.take();
1419     }
1420 
1421     // ---- bulk edits ----
1422 
1423     /// Rebuild around a new point order: point `n` of the result is point
1424     /// `idx[n]` of this one. Identities, positions, point attributes and point
1425     /// groups all follow. Primitives are rewired through the inverse map, and
1426     /// any primitive referencing a dropped point is dropped with it — a
1427     /// half-referenced polygon is not geometry.
1428     pub fn gather_points(&mut self, idx: &[u32]) {
1429         let mut inverse = vec![u32::MAX; self.num_points()];
1430         for (new, &old) in idx.iter().enumerate() {
1431             if let Some(slot) = inverse.get_mut(old as usize) {
1432                 // A point appearing twice keeps its first landing place; the
1433                 // duplicate still exists, it is simply not what primitives
1434                 // point at.
1435                 if *slot == u32::MAX {
1436                     *slot = new as u32;
1437                 }
1438             }
1439         }
1440 
1441         self.pos = idx
1442             .iter()
1443             .map(|&i| self.pos.get(i as usize).copied().unwrap_or([0.0; 3]))
1444             .collect();
1445         self.ids = idx
1446             .iter()
1447             .map(|&i| self.ids.get(i as usize).copied().unwrap_or(0))
1448             .collect();
1449         self.points = self.points.gather(idx);
1450 
1451         let mut vert_point = Vec::with_capacity(self.vert_point.len());
1452         let mut prim_start = vec![0u32];
1453         let mut kept_prims: Vec<u32> = Vec::new();
1454         let mut kept_verts: Vec<u32> = Vec::new();
1455         for prim in 0..self.num_prims() {
1456             let range = self.prim_verts(prim);
1457             let survives = self.vert_point[range.clone()]
1458                 .iter()
1459                 .all(|&pt| inverse.get(pt as usize).copied().unwrap_or(u32::MAX) != u32::MAX);
1460             if !survives {
1461                 continue;
1462             }
1463             for v in range {
1464                 kept_verts.push(v as u32);
1465                 vert_point.push(inverse[self.vert_point[v] as usize]);
1466             }
1467             prim_start.push(vert_point.len() as u32);
1468             kept_prims.push(prim as u32);
1469         }
1470 
1471         self.verts = self.verts.gather(&kept_verts);
1472         self.prims = self.prims.gather(&kept_prims);
1473         self.vert_point = vert_point;
1474         self.prim_start = prim_start;
1475         self.invalidate();
1476     }
1477 
1478     /// Merge points onto representatives: point `p` becomes `rep[p]`.
1479     ///
1480     /// A representative keeps its identity and its values — the same choice
1481     /// the remesher's collapse makes, and for the same reason: one of the two
1482     /// is a point the solver has been writing to, and the merge should cost
1483     /// the simulation as little memory as it can. Primitives are rewired, and
1484     /// one left with a repeated corner is dropped, because a triangle with two
1485     /// corners in the same place is not a triangle.
1486     ///
1487     /// Chains are followed, so `rep` need not already be flat: a fuse that
1488     /// pointed a at b and b at c leaves everything at c.
1489     pub fn fuse_points(&mut self, rep: &[u32]) {
1490         let n = self.num_points();
1491         let root = |mut p: u32| {
1492             // Bounded rather than trusting the map to be acyclic: a cycle in a
1493             // caller's representative map would otherwise hang the app.
1494             for _ in 0..n {
1495                 let next = rep.get(p as usize).copied().unwrap_or(p);
1496                 if next == p {
1497                     break;
1498                 }
1499                 p = next;
1500             }
1501             p
1502         };
1503         for v in self.vert_point.iter_mut() {
1504             *v = root(*v);
1505         }
1506 
1507         // A primitive whose corners collapsed onto each other is not a
1508         // primitive any more. Dropped here rather than left for the point
1509         // compaction, which only knows about points that went away — these
1510         // ones all still exist, they have just stopped being distinct.
1511         let mut vert_point = Vec::with_capacity(self.vert_point.len());
1512         let mut prim_start = vec![0u32];
1513         let mut kept_prims: Vec<u32> = Vec::new();
1514         let mut kept_verts: Vec<u32> = Vec::new();
1515         for prim in 0..self.num_prims() {
1516             let range = self.prim_verts(prim);
1517             let pts = &self.vert_point[range.clone()];
1518             let mut uniq = pts.to_vec();
1519             uniq.sort_unstable();
1520             uniq.dedup();
1521             if uniq.len() < pts.len() || uniq.len() < 3 {
1522                 continue;
1523             }
1524             for v in range {
1525                 kept_verts.push(v as u32);
1526                 vert_point.push(self.vert_point[v]);
1527             }
1528             prim_start.push(vert_point.len() as u32);
1529             kept_prims.push(prim as u32);
1530         }
1531         self.verts = self.verts.gather(&kept_verts);
1532         self.prims = self.prims.gather(&kept_prims);
1533         self.vert_point = vert_point;
1534         self.prim_start = prim_start;
1535 
1536         let keep: Vec<bool> = (0..n).map(|p| root(p as u32) as usize == p).collect();
1537         self.invalidate();
1538         self.keep_points(&keep);
1539     }
1540 
1541     /// Keep the points `keep` marks true, dropping the rest.
1542     pub fn keep_points(&mut self, keep: &[bool]) {
1543         let idx: Vec<u32> = (0..self.num_points() as u32)
1544             .filter(|&i| keep.get(i as usize).copied().unwrap_or(false))
1545             .collect();
1546         self.gather_points(&idx);
1547     }
1548 
1549     /// Append `other`. Identities are reallocated on the way in, so two pieces
1550     /// of geometry that were generated independently — and therefore both
1551     /// number their points from zero — do not collide.
1552     pub fn merge(&mut self, other: &Detail) {
1553         // Merged into nothing, the result has `other`'s structure exactly —
1554         // the same points and primitives, only the identities renumbered —
1555         // so its topology is `other`'s, built or not. The scene is
1556         // assembled this way, one displayed node into an empty detail.
1557         let shared = if self.num_points() == 0 && self.num_prims() == 0 { other.topo.get().cloned() } else { None };
1558         let point_offset = self.num_points() as u32;
1559         let vert_offset = self.vert_point.len() as u32;
1560 
1561         self.pos.extend_from_slice(&other.pos);
1562         for _ in 0..other.num_points() {
1563             self.ids.push(self.next_id);
1564             self.next_id += 1;
1565         }
1566         self.points.append(&other.points);
1567 
1568         self.vert_point
1569             .extend(other.vert_point.iter().map(|&p| p + point_offset));
1570         self.verts.append(&other.verts);
1571 
1572         for w in other.prim_start.iter().skip(1) {
1573             self.prim_start.push(w + vert_offset);
1574         }
1575         self.prims.append(&other.prims);
1576 
1577         self.invalidate();
1578         if let Some(t) = shared {
1579             let _ = self.topo.set(t);
1580         }
1581     }
1582 
1583     /// [`merge`](Self::merge) of a detail the caller is done with. Into an
1584     /// EMPTY detail — which is how the scene is assembled, each level's
1585     /// displayed node into a fresh one — it takes `other`'s arrays instead
1586     /// of copying them: the same result to the last value, the identities
1587     /// renumbered and the detail attributes left behind as `merge` leaves
1588     /// them (`merge_owned_is_merge`). Until 2026-10-07 the scene walk copied
1589     /// a 57k-point simulation's whole state twice a frame this way. Into
1590     /// anything else it is `merge`.
1591     pub fn merge_owned(&mut self, other: Detail) {
1592         let empty = self.pos.is_empty()
1593             && self.vert_point.is_empty()
1594             && self.prim_start.len() <= 1
1595             && self.points.is_bare()
1596             && self.verts.is_bare()
1597             && self.prims.is_bare();
1598         if !empty {
1599             self.merge(&other);
1600             return;
1601         }
1602         let Detail { pos, vert_point, prim_start, points, verts, prims, topo, .. } = other;
1603         let first = self.next_id;
1604         self.ids = (0..pos.len() as PointId).map(|i| first + i).collect();
1605         self.next_id = first + pos.len() as PointId;
1606         self.pos = pos;
1607         self.points = points;
1608         self.vert_point = vert_point;
1609         self.verts = verts;
1610         self.prim_start = prim_start;
1611         self.prims = prims;
1612         self.topo = topo;
1613     }
1614 
1615     // ---- convenience ----
1616 
1617     /// Point color, from `Cd` where it exists.
1618     pub fn color(&self, p: usize) -> [f32; 3] {
1619         match self.points.value(CD, p) {
1620             Some(AttribValue::Float3(c)) => c,
1621             Some(other) => other.as_vec3().to_array(),
1622             None => DEFAULT_COLOR,
1623         }
1624     }
1625 
1626     /// Set point color, creating `Cd` if this is the first writer.
1627     pub fn set_color(&mut self, p: usize, c: [f32; 3]) {
1628         self.points
1629             .get_or_create(CD, AttribValue::Float3(DEFAULT_COLOR));
1630         let _ = self.points.set_value(CD, p, AttribValue::Float3(c));
1631     }
1632 
1633     /// Whether the surface is closed: every directed edge has exactly one
1634     /// opposite.
1635     ///
1636     /// The question "what is inside this?" only has an answer for a closed
1637     /// surface. A flat disc, a torn mesh or a single polygon has no inside, and
1638     /// anything that signs a distance field has to know the difference — sign
1639     /// an open surface and you get whichever side its normals happen to face,
1640     /// which is not a solid, just a preference.
1641     ///
1642     /// Directed, not undirected, because the property that matters is "no
1643     /// boundary, consistently wound", and those are the same test: every edge
1644     /// walked one way by one face and the other way by its neighbour. Counting
1645     /// undirected edges instead would ask for exactly two faces per edge, which
1646     /// is [`is_manifold`](Self::is_manifold) — a stricter thing that a boolean
1647     /// legitimately fails where two sheets pinch together along a knife edge
1648     /// thinner than a voxel. Such a surface is still watertight, still has an
1649     /// inside, and still voxelizes correctly.
1650     pub fn is_closed(&self) -> bool {
1651         if self.num_prims() == 0 {
1652             return false;
1653         }
1654         let mut counts: HashMap<[u32; 2], i32> = HashMap::new();
1655         for prim in 0..self.num_prims() {
1656             let pts = self.prim_points(prim);
1657             if pts.len() < 3 {
1658                 return false;
1659             }
1660             for i in 0..pts.len() {
1661                 let (a, b) = (pts[i], pts[(i + 1) % pts.len()]);
1662                 // One counter per undirected edge, incremented one way and
1663                 // decremented the other: it lands on zero exactly when every
1664                 // traversal is matched by an opposite one.
1665                 let (key, step) = if a < b { ([a, b], 1) } else { ([b, a], -1) };
1666                 *counts.entry(key).or_default() += step;
1667             }
1668         }
1669         counts.values().all(|&c| c == 0)
1670     }
1671 
1672     /// Whether every edge is shared by exactly two primitives.
1673     ///
1674     /// Stricter than [`is_closed`](Self::is_closed): it also rules out the
1675     /// place where more than two faces meet along one edge. Remeshing wants
1676     /// this — an edge with four faces has no single pair to flip or collapse
1677     /// between — while voxelizing does not.
1678     pub fn is_manifold(&self) -> bool {
1679         if self.num_prims() == 0 {
1680             return false;
1681         }
1682         let mut counts: HashMap<[u32; 2], usize> = HashMap::new();
1683         for prim in 0..self.num_prims() {
1684             let pts = self.prim_points(prim);
1685             if pts.len() < 3 {
1686                 return false;
1687             }
1688             for i in 0..pts.len() {
1689                 let (a, b) = (pts[i], pts[(i + 1) % pts.len()]);
1690                 *counts.entry([a.min(b), a.max(b)]).or_default() += 1;
1691             }
1692         }
1693         counts.values().all(|&c| c == 2)
1694     }
1695 
1696     /// The axis-aligned bounds, or `None` when there are no points.
1697     pub fn bounds(&self) -> Option<(Vec3, Vec3)> {
1698         let first = *self.pos.first()?;
1699         let (mut lo, mut hi) = (Vec3::from(first), Vec3::from(first));
1700         for p in &self.pos[1..] {
1701             let v = Vec3::from(*p);
1702             lo = lo.min(v);
1703             hi = hi.max(v);
1704         }
1705         Some((lo, hi))
1706     }
1707 
1708     /// Zero every Derivative attribute on every class — the step boundary's
1709     /// first act. See [`AttribKind`].
1710     pub fn clear_derivatives(&mut self) {
1711         self.points.clear_derivatives();
1712         self.verts.clear_derivatives();
1713         self.prims.clear_derivatives();
1714         self.detail.clear_derivatives();
1715     }
1716 
1717     /// Restore this geometry's Live point attributes from `prev`, matching
1718     /// points by identity.
1719     ///
1720     /// The other half of the contract, and the one that makes a rebuild safe
1721     /// to put in the middle of a solve. When a step's chain hands back geometry
1722     /// that has lost an attribute — a kernel generator that rebuilt its points,
1723     /// and in Phase 3 a remesh — the values are not gone, they are in the
1724     /// previous state, attached to identities. A point that survived gets its
1725     /// value back; a point that is genuinely new gets the type's zero, which is
1726     /// the only honest answer for a place that did not exist last step.
1727     ///
1728     /// Attributes the new geometry DOES carry are left alone: the chain
1729     /// computed them this step and that is the whole point of running it.
1730     /// Derivative attributes are not restored at all — they are meant to be
1731     /// rebuilt, and carrying one across would be exactly the silent
1732     /// accumulation the kind exists to prevent.
1733     pub fn restore_live_from(&mut self, prev: &Detail) {
1734         // Restoration bridges a REBUILD, not a deletion. If the chain handed
1735         // back the same identities in the same order, it kept the geometry it
1736         // was given — so an attribute that is gone was taken out on purpose,
1737         // and putting it back would override the author. Only when the point
1738         // set itself changed underneath is a missing attribute evidence of
1739         // loss rather than intent.
1740         if self.ids == prev.ids {
1741             return;
1742         }
1743         let missing: Vec<&str> = prev
1744             .points
1745             .names_of_kind(AttribKind::Live)
1746             .into_iter()
1747             .filter(|n| !self.points.has(n))
1748             .collect();
1749         if missing.is_empty() {
1750             return;
1751         }
1752         let was: HashMap<PointId, u32> = prev.id_map();
1753         for name in missing {
1754             let Some(src) = prev.points.get(name) else { continue };
1755             let ty = src.ty();
1756             let mut data = AttribData::zeroed(ty, self.num_points());
1757             for p in 0..self.num_points() {
1758                 let Some(id) = self.id(p) else { continue };
1759                 let Some(&old) = was.get(&id) else { continue };
1760                 if let Some(v) = src.get(old as usize) {
1761                     let _ = data.set(p, v);
1762                 }
1763             }
1764             let _ = self.points.insert(name, data);
1765             self.points.set_kind(name, AttribKind::Live);
1766         }
1767     }
1768 
1769     /// Serialize to a compact binary blob.
1770     ///
1771     /// Hand-rolled rather than derived, because the one thing a cache is for is
1772     /// being cheaper than recomputing: a hundred thousand points of JSON text
1773     /// is not. Positions and attribute arrays go out as raw little-endian
1774     /// floats, which is also how they sit in memory.
1775     ///
1776     /// The derived topology is NOT written — it is rebuilt from the primitives
1777     /// on read, and storing it would mean a file that can disagree with itself.
1778     pub fn to_bytes(&self) -> Vec<u8> {
1779         let mut out = Vec::new();
1780         out.extend_from_slice(DETAIL_MAGIC);
1781         put_u32(&mut out, self.pos.len() as u32);
1782         put_u64(&mut out, self.next_id);
1783         for p in &self.pos {
1784             for c in p {
1785                 out.extend_from_slice(&c.to_le_bytes());
1786             }
1787         }
1788         for id in &self.ids {
1789             put_u64(&mut out, *id);
1790         }
1791         put_u32(&mut out, self.vert_point.len() as u32);
1792         for v in &self.vert_point {
1793             put_u32(&mut out, *v);
1794         }
1795         put_u32(&mut out, self.prim_start.len() as u32);
1796         for v in &self.prim_start {
1797             put_u32(&mut out, *v);
1798         }
1799         for store in [&self.points, &self.verts, &self.prims, &self.detail] {
1800             store.write_into(&mut out);
1801         }
1802         out
1803     }
1804 
1805     /// Read back a blob written by [`Detail::to_bytes`].
1806     ///
1807     /// Every length is checked against what is actually left in the buffer, so
1808     /// a truncated or corrupt cache file is an error rather than a huge
1809     /// allocation or a panic. A cache lives in a directory anything can write
1810     /// to, and must never be trusted the way a value from memory is.
1811     pub fn from_bytes(bytes: &[u8]) -> Result<Detail, String> {
1812         let mut r = Reader { b: bytes, at: 0 };
1813         if r.take(DETAIL_MAGIC.len())? != DETAIL_MAGIC {
1814             return Err("not a Detail blob".into());
1815         }
1816         let num_points = r.u32()? as usize;
1817         let next_id = r.u64()?;
1818         let mut pos = Vec::with_capacity(num_points.min(1 << 20));
1819         for _ in 0..num_points {
1820             pos.push([r.f32()?, r.f32()?, r.f32()?]);
1821         }
1822         let mut ids = Vec::with_capacity(pos.len());
1823         for _ in 0..num_points {
1824             ids.push(r.u64()?);
1825         }
1826         let nv = r.u32()? as usize;
1827         let mut vert_point = Vec::with_capacity(nv.min(1 << 20));
1828         for _ in 0..nv {
1829             vert_point.push(r.u32()?);
1830         }
1831         let ns = r.u32()? as usize;
1832         let mut prim_start = Vec::with_capacity(ns.min(1 << 20));
1833         for _ in 0..ns {
1834             prim_start.push(r.u32()?);
1835         }
1836         if prim_start.is_empty() {
1837             return Err("primitive offsets are missing their terminator".into());
1838         }
1839         let points = AttribStore::read_from(&mut r)?;
1840         let verts = AttribStore::read_from(&mut r)?;
1841         let prims = AttribStore::read_from(&mut r)?;
1842         let detail = AttribStore::read_from(&mut r)?;
1843 
1844         // Cross-checks, because every reader below indexes on these being
1845         // consistent and a corrupt file must not reach that code.
1846         if points.len() != num_points || verts.len() != nv || prims.len() != ns - 1 {
1847             return Err("element counts disagree with their attribute stores".into());
1848         }
1849         if vert_point.iter().any(|&p| p as usize >= num_points.max(1)) && num_points > 0 {
1850             return Err("a vertex references a point that is not there".into());
1851         }
1852         Ok(Detail {
1853             pos,
1854             ids,
1855             next_id,
1856             points,
1857             vert_point,
1858             verts,
1859             prim_start,
1860             prims,
1861             detail,
1862             topo: OnceLock::new(),
1863         })
1864     }
1865 
1866     /// Weld a triangle soup into points and triangles: coincident positions
1867     /// become one point, every three positions become one primitive.
1868     ///
1869     /// The migration path for generators that still emit soup, and a faithful
1870     /// port of `geometry::weld_points` — including its 1e-4 quantization, so
1871     /// that vertices a kernel emitted from the same formula weld reliably.
1872     /// Color is carried onto `Cd`, taking the first copy of each welded point.
1873     pub fn from_triangle_soup(positions: &[[f32; 3]], colors: &[[f32; 3]]) -> Detail {
1874         Self::from_triangle_soup_with_map(positions, colors).0
1875     }
1876 
1877     /// [`Detail::from_triangle_soup`], plus the point each input corner welded
1878     /// onto — so a caller holding per-corner data can carry it across.
1879     pub fn from_triangle_soup_with_map(
1880         positions: &[[f32; 3]],
1881         colors: &[[f32; 3]],
1882     ) -> (Detail, Vec<u32>) {
1883         let mut detail = Detail::new();
1884         let mut key_to_point: HashMap<(i64, i64, i64), u32> = HashMap::new();
1885         let mut point_of: Vec<u32> = Vec::with_capacity(positions.len());
1886         let mut cd: Vec<[f32; 3]> = Vec::new();
1887 
1888         for (i, p) in positions.iter().enumerate() {
1889             let key = (
1890                 (p[0] as f64 * 1e4).round() as i64,
1891                 (p[1] as f64 * 1e4).round() as i64,
1892                 (p[2] as f64 * 1e4).round() as i64,
1893             );
1894             let idx = match key_to_point.get(&key) {
1895                 Some(&idx) => idx,
1896                 None => {
1897                     let idx = detail.add_point(Vec3::from(*p));
1898                     key_to_point.insert(key, idx);
1899                     cd.push(colors.get(i).copied().unwrap_or(DEFAULT_COLOR));
1900                     idx
1901                 }
1902             };
1903             point_of.push(idx);
1904         }
1905 
1906         for tri in point_of.chunks_exact(3) {
1907             detail.add_prim(tri);
1908         }
1909 
1910         if !cd.is_empty() {
1911             detail
1912                 .points
1913                 .attribs
1914                 .insert(CD.to_string(), AttribData::Float3(cd));
1915         }
1916         (detail, point_of)
1917     }
1918 
1919     /// The point behind every corner [`Detail::triangulate`] emits, in the
1920     /// same order.
1921     ///
1922     /// Lets a caller that had to flatten to triangles — the OpenCL launcher once,
1923     /// until the Phase 1 ABI binds attributes directly — put results back on
1924     /// the points they came from instead of welding the output and losing
1925     /// every identity.
1926     pub fn triangulate_points(&self) -> Vec<u32> {
1927         let mut out = Vec::new();
1928         for prim in 0..self.num_prims() {
1929             let pts = self.prim_points(prim);
1930             if pts.len() < 3 {
1931                 continue;
1932             }
1933             for i in 1..pts.len() - 1 {
1934                 out.extend_from_slice(&[pts[0], pts[i], pts[i + 1]]);
1935             }
1936         }
1937         out
1938     }
1939 
1940     /// Fan-triangulate every primitive, handing each corner to `make` as
1941     /// (position, color).
1942     ///
1943     /// The render boundary. It takes a closure rather than returning the
1944     /// renderer's vertex type so that this module stays free of anything that
1945     /// draws — the caller in `geometry.rs` supplies `Vertex3D`.
1946     pub fn triangulate<V>(&self, mut make: impl FnMut([f32; 3], [f32; 3]) -> V) -> Vec<V> {
1947         let colors = self.point_colors();
1948         let mut out = Vec::new();
1949         for prim in 0..self.num_prims() {
1950             let pts = self.prim_points(prim);
1951             if pts.len() < 3 {
1952                 continue;
1953             }
1954             for i in 1..pts.len() - 1 {
1955                 for &p in &[pts[0], pts[i], pts[i + 1]] {
1956                     let p = p as usize;
1957                     out.push(make(
1958                         self.pos.get(p).copied().unwrap_or([0.0; 3]),
1959                         colors.get(p).copied().unwrap_or(DEFAULT_COLOR),
1960                     ));
1961                 }
1962             }
1963         }
1964         out
1965     }
1966 }