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 }