1use fast_mesh_store::{ChangeSet, Mesh, StoreError, TriangleId, VertexId};
4
5use crate::scene::Entry;
6use crate::{MeshId, Result, Scene, SceneError};
7
8fn oom() -> SceneError {
9 SceneError::Store(StoreError::OutOfMemory)
10}
11
12fn try_resize<T: Copy>(v: &mut Vec<T>, len: usize, fill: T) -> Result<()> {
15 v.try_reserve(len.saturating_sub(v.len()))
16 .map_err(|_| oom())?;
17 v.resize(len, fill);
18 Ok(())
19}
20
21fn face_normal(p: &[[f32; 3]], tri: [VertexId; 3]) -> [f32; 3] {
24 let [a, b, c] = tri.map(|v| p[v as usize]);
25 let e1 = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
26 let e2 = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
27 [
28 e1[1] * e2[2] - e1[2] * e2[1],
29 e1[2] * e2[0] - e1[0] * e2[2],
30 e1[0] * e2[1] - e1[1] * e2[0],
31 ]
32}
33
34fn push(dirty: &mut Vec<VertexId>, vs: &[VertexId]) -> Result<()> {
36 dirty.try_reserve(vs.len()).map_err(|_| oom())?;
37 dirty.extend_from_slice(vs);
38 Ok(())
39}
40
41fn add(sum: &mut [f32; 3], n: [f32; 3]) {
42 for k in 0..3 {
43 sum[k] += n[k];
44 }
45}
46
47fn normalized(n: [f32; 3]) -> [f32; 3] {
49 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
50 if len > 0.0 { n.map(|x| x / len) } else { n }
51}
52
53pub(crate) struct NormalState {
56 normals: Vec<[f32; 3]>,
57 triangles: Vec<[VertexId; 3]>,
61 pending: Option<ChangeSet>,
63}
64
65impl NormalState {
66 pub(crate) fn new(mesh: &Mesh) -> Result<NormalState> {
68 let mut state = NormalState {
69 normals: Vec::new(),
70 triangles: Vec::new(),
71 pending: None,
72 };
73 state.compute_all(mesh)?;
74 Ok(state)
75 }
76
77 pub(crate) fn normals(&self) -> &[[f32; 3]] {
78 &self.normals
79 }
80
81 pub(crate) fn record(&mut self, changes: &ChangeSet, vertex_slots: usize) {
86 if let Some(p) = &mut self.pending {
87 p.merge(changes);
88 }
89 if vertex_slots > self.normals.len()
90 && try_resize(&mut self.normals, vertex_slots, [0.0; 3]).is_err()
91 {
92 self.pending = None;
93 }
94 }
95
96 pub(crate) fn mark_all(&mut self) {
99 self.pending = None;
100 }
101
102 fn compute_all(&mut self, mesh: &Mesh) -> Result<()> {
107 let mut build = NormalBuild::new();
108 while !build.step(mesh, usize::MAX)? {}
109 *self = build.finish().ok_or_else(oom)?;
110 Ok(())
111 }
112
113 fn compute_changed(&mut self, mesh: &mut Mesh, c: &ChangeSet) -> Result<Option<Vec<VertexId>>> {
123 let verts = c.vertices_moved.len() + c.vertices_added.len() + c.vertices_removed.len();
124 let tris = c.triangles_added.len() + c.triangles_removed.len() + c.triangles_changed.len();
125 if verts > mesh.vertex_slots() / 2 || tris > mesh.triangle_slots() / 2 {
126 self.compute_all(mesh)?;
127 return Ok(None);
128 }
129 mesh.ensure_incidence()?;
131 let (slots, tri_slots) = (mesh.vertex_slots(), mesh.triangle_slots());
132 try_resize(&mut self.normals, slots, [0.0; 3])?;
133 try_resize(&mut self.triangles, tri_slots, [0; 3])?;
134 let mesh = &*mesh;
135 let inc = mesh.incidence().expect("built above");
136 let tris = mesh.triangles();
137 let mut dirty: Vec<VertexId> = Vec::new();
138 for &t in c.triangles_removed.iter().chain(&c.triangles_changed) {
139 if let Some(old) = self.triangles.get(t as usize) {
140 push(&mut dirty, old)?;
141 }
142 }
143 for &t in c.triangles_added.iter().chain(&c.triangles_changed) {
144 if let Some(tri) = mesh.triangle(t) {
145 push(&mut dirty, &tri)?;
146 }
147 }
148 push(&mut dirty, &c.vertices_added)?;
149 push(&mut dirty, &c.vertices_removed)?;
150 push(&mut dirty, &c.vertices_moved)?;
151 for &v in &c.vertices_moved {
152 for &t in inc.triangles(v) {
153 push(&mut dirty, &tris[t as usize])?;
154 }
155 }
156 dirty.sort_unstable();
157 dirty.dedup();
158 dirty.retain(|&v| (v as usize) < slots);
159 let p = mesh.positions();
162 for &v in &dirty {
163 let mut sum = [0.0f32; 3];
164 for &t in inc.triangles(v) {
165 let tri = tris[t as usize];
166 let n = face_normal(p, tri);
167 for &w in &tri {
170 if w == v {
171 add(&mut sum, n);
172 }
173 }
174 }
175 self.normals[v as usize] = normalized(sum);
176 }
177 for &t in c
178 .triangles_added
179 .iter()
180 .chain(&c.triangles_removed)
181 .chain(&c.triangles_changed)
182 {
183 if let Some(slot) = self.triangles.get_mut(t as usize) {
184 *slot = tris[t as usize];
185 }
186 }
187 self.pending = Some(ChangeSet::default());
188 Ok(Some(dirty))
189 }
190}
191
192const VERTICES_PER_UNIT: usize = 16;
195
196const TRIANGLES_PER_UNIT: usize = 4;
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201enum NormalStage {
202 Zero(usize),
204 Sum(usize),
206 Normalize(usize),
208 Done,
209}
210
211#[derive(Debug)]
217pub(crate) struct NormalBuild {
218 normals: Vec<[f32; 3]>,
219 triangles: Vec<[VertexId; 3]>,
220 stage: NormalStage,
221}
222
223impl NormalBuild {
224 pub(crate) fn new() -> NormalBuild {
225 NormalBuild {
226 normals: Vec::new(),
227 triangles: Vec::new(),
228 stage: NormalStage::Zero(0),
229 }
230 }
231
232 pub(crate) fn is_done(&self) -> bool {
233 self.stage == NormalStage::Done
234 }
235
236 pub(crate) fn normals(&self) -> &[[f32; 3]] {
238 &self.normals
239 }
240
241 pub(crate) fn step(&mut self, mesh: &Mesh, budget: usize) -> Result<bool> {
251 let mut left = budget.max(1);
252 let (slots, tri_slots) = (mesh.vertex_slots(), mesh.triangle_slots());
253 let take = |left: &mut usize, from: usize, end: usize, per: usize| {
255 let to = end.min(from.saturating_add(left.saturating_mul(per)));
256 *left = left.saturating_sub((to - from).div_ceil(per));
257 to
258 };
259 loop {
260 match self.stage {
261 NormalStage::Zero(from) => {
262 if from == 0 {
263 self.normals.try_reserve_exact(slots).map_err(|_| oom())?;
264 self.triangles
265 .try_reserve_exact(tri_slots)
266 .map_err(|_| oom())?;
267 }
268 let end = slots + tri_slots;
271 let to = if from < slots {
272 take(&mut left, from, slots, VERTICES_PER_UNIT)
273 } else {
274 take(&mut left, from, end, TRIANGLES_PER_UNIT)
275 };
276 if from < slots {
277 self.normals.resize(to, [0.0; 3]);
278 } else {
279 self.triangles
280 .extend_from_slice(&mesh.triangles()[from - slots..to - slots]);
281 }
282 self.stage = if to == end {
283 NormalStage::Sum(0)
284 } else {
285 NormalStage::Zero(to)
286 };
287 }
288 NormalStage::Sum(from) => {
289 let to = take(&mut left, from, tri_slots, TRIANGLES_PER_UNIT);
290 let p = mesh.positions();
291 for (t, &tri) in self.triangles.iter().enumerate().take(to).skip(from) {
292 if mesh.is_triangle_alive(t as TriangleId) {
293 let n = face_normal(p, tri);
294 for v in tri {
295 add(&mut self.normals[v as usize], n);
296 }
297 }
298 }
299 self.stage = if to == tri_slots {
300 NormalStage::Normalize(0)
301 } else {
302 NormalStage::Sum(to)
303 };
304 }
305 NormalStage::Normalize(from) => {
306 let to = take(&mut left, from, slots, VERTICES_PER_UNIT);
307 for n in &mut self.normals[from..to] {
308 *n = normalized(*n);
309 }
310 self.stage = if to == slots {
311 NormalStage::Done
312 } else {
313 NormalStage::Normalize(to)
314 };
315 }
316 NormalStage::Done => return Ok(true),
317 }
318 if left == 0 {
319 return Ok(self.is_done());
320 }
321 }
322 }
323
324 pub(crate) fn finish(self) -> Option<NormalState> {
327 self.is_done().then_some(NormalState {
328 normals: self.normals,
329 triangles: self.triangles,
330 pending: Some(ChangeSet::default()),
331 })
332 }
333}
334
335impl Entry {
336 pub(crate) fn report_normals(&mut self, ids: Option<&[VertexId]>) {
338 self.content += 1;
339 for r in &mut self.reports {
340 r.normals_changed(ids);
341 }
342 }
343
344 pub(crate) fn update_normals(&mut self) -> Result<()> {
348 let Some(ns) = &mut self.normals else {
349 return Ok(());
350 };
351 match ns.pending.take() {
352 None => {
353 if let Err(e) = ns.compute_all(&self.mesh) {
354 ns.pending = None;
355 return Err(e);
356 }
357 self.report_normals(None);
358 }
359 Some(c) if c.is_empty() && ns.normals.len() == self.mesh.vertex_slots() => {
360 ns.pending = Some(c);
361 }
362 Some(c) => match ns.compute_changed(&mut self.mesh, &c) {
363 Ok(dirty) => self.report_normals(dirty.as_deref()),
364 Err(e) => {
365 ns.pending = Some(c);
366 return Err(e);
367 }
368 },
369 }
370 Ok(())
371 }
372}
373
374impl Scene {
375 pub fn set_normals(&mut self, id: MeshId, on: bool) -> Result<()> {
394 self.drop_field_unit();
395 let entry = self.entries.get_mut(id).ok_or(SceneError::UnknownMesh)?;
396 if on == entry.normals.is_some() {
397 return Ok(());
398 }
399 entry.normals = if on {
400 Some(NormalState::new(&entry.mesh)?)
401 } else {
402 None
403 };
404 entry.report_normals(None);
405 Ok(())
406 }
407
408 pub fn normals(&self, id: MeshId) -> Result<Option<&[[f32; 3]]>> {
423 Ok(self.entry(id)?.normals.as_ref().map(NormalState::normals))
424 }
425
426 #[doc(hidden)]
436 pub fn content_stamp(&self, id: MeshId) -> Result<u64> {
437 Ok(self.entry(id)?.content)
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use fast_mesh_store::{Mesh, TriangleId, VertexId};
444
445 use crate::{MeshId, MeshOptions, Scene, SceneError, SceneOptions, UpdateMode};
446
447 fn wavy(n: u32) -> Mesh {
450 let mut pos = Vec::new();
451 for y in 0..=n {
452 for x in 0..=n {
453 let (fx, fy) = (x as f32, y as f32);
454 pos.extend_from_slice(&[fx, fy, 0.3 * (fx * 0.9).sin() * (fy * 0.7).cos()]);
455 }
456 }
457 let mut idx = Vec::new();
458 for y in 0..n {
459 for x in 0..n {
460 let a = y * (n + 1) + x;
461 let (b, c) = (a + 1, a + n + 1);
462 idx.extend_from_slice(&[a, b, c, b, c + 1, c]);
463 }
464 }
465 Mesh::from_buffers(&pos, &idx).unwrap()
466 }
467
468 fn scene_with(mode: UpdateMode) -> (Scene, MeshId) {
469 let mut scene = Scene::new(SceneOptions::default());
470 let options = MeshOptions {
471 mode,
472 ..MeshOptions::default()
473 };
474 let id = scene.add_mesh(wavy(6), options).unwrap();
475 (scene, id)
476 }
477
478 fn reference(mesh: &Mesh) -> Vec<[f32; 3]> {
480 let flat: Vec<f32> = mesh.positions().iter().flatten().copied().collect();
481 let idx: Vec<u32> = (0..mesh.triangle_slots() as TriangleId)
482 .filter_map(|t| mesh.triangle(t))
483 .flatten()
484 .collect();
485 let n = fast_mesh::compute_vertex_normals(&flat, &idx).unwrap();
486 n.chunks(3).map(|c| [c[0], c[1], c[2]]).collect()
487 }
488
489 fn assert_matches(scene: &Scene, id: MeshId) {
490 let mesh = scene.mesh(id).unwrap();
491 let got = scene.normals(id).unwrap().expect("normals are on");
492 let want = reference(mesh);
493 assert_eq!(got.len(), mesh.vertex_slots());
494 for (v, (g, w)) in got.iter().zip(&want).enumerate() {
495 for k in 0..3 {
496 assert!((g[k] - w[k]).abs() <= 1e-6, "vertex {v}: {g:?} vs {w:?}");
497 }
498 }
499 }
500
501 fn normals_dirty(scene: &mut Scene, id: MeshId) -> Option<Vec<VertexId>> {
502 scene
503 .take_changes()
504 .into_iter()
505 .find(|c| c.id == id)
506 .map_or(Some(Vec::new()), |c| c.normals_dirty)
507 }
508
509 fn move_by(scene: &mut Scene, id: MeshId, v: VertexId, d: [f32; 3]) {
510 scene
511 .edit(id, |tx| {
512 let p = tx.mesh().position(v).unwrap();
513 tx.set_positions(&[v], &[std::array::from_fn(|k| p[k] + d[k])])
514 })
515 .unwrap();
516 }
517
518 fn ring_of_ring(mesh: &Mesh, v: VertexId) -> Vec<VertexId> {
520 let mut out: Vec<VertexId> = (0..mesh.triangle_slots() as TriangleId)
521 .filter_map(|t| mesh.triangle(t))
522 .filter(|tri| tri.contains(&v))
523 .flatten()
524 .collect();
525 out.sort_unstable();
526 out.dedup();
527 out
528 }
529
530 #[test]
531 fn normals_match_compute_vertex_normals_on_add() {
532 let (mut scene, id) = scene_with(UpdateMode::Eager);
533 scene.set_normals(id, true).unwrap();
534 assert_matches(&scene, id);
535 let n = scene.normals(id).unwrap().unwrap();
536 assert!(n.iter().all(|n| n[2] > 0.0), "upward wound grid");
537 }
538
539 #[test]
540 fn normals_follow_vertex_move() {
541 let (mut scene, id) = scene_with(UpdateMode::Eager);
542 scene.set_normals(id, true).unwrap();
543 scene.take_changes();
544 let v = 3 * 7 + 3;
545 move_by(&mut scene, id, v, [0.1, -0.2, 0.7]);
546 let want = ring_of_ring(scene.mesh(id).unwrap(), v);
547 assert!(want.len() > 1 && want.len() < 20, "{want:?}");
548 assert_eq!(normals_dirty(&mut scene, id), Some(want));
549 assert_matches(&scene, id);
550 scene.undo().unwrap();
552 let want = ring_of_ring(scene.mesh(id).unwrap(), v);
553 assert_eq!(normals_dirty(&mut scene, id), Some(want));
554 assert_matches(&scene, id);
555 let mesh = scene.mesh(id).unwrap();
557 let old = mesh.triangle(10).unwrap();
558 scene.edit(id, |tx| tx.remove_triangles(&[10])).unwrap();
559 let mut want = old.to_vec();
560 want.sort_unstable();
561 assert_eq!(normals_dirty(&mut scene, id), Some(want));
562 assert_matches(&scene, id);
563 let mesh = scene.mesh(id).unwrap();
564 let t = (0..mesh.triangle_slots() as TriangleId)
565 .find(|&t| mesh.is_triangle_alive(t))
566 .unwrap();
567 let [a, b, c] = mesh.triangle(t).unwrap();
568 let far = 6 * 7 + 6;
569 scene
570 .edit(id, |tx| tx.set_triangles(&[t], &[[a, b, far]]))
571 .unwrap();
572 let mut want = vec![a, b, c, far];
573 want.sort_unstable();
574 want.dedup();
575 assert_eq!(normals_dirty(&mut scene, id), Some(want));
576 assert_matches(&scene, id);
577 let mut p = scene.placement(id).unwrap();
579 p.translation = [5.0, 0.0, 0.0];
580 scene.set_placement(id, p).unwrap();
581 assert_eq!(normals_dirty(&mut scene, id), Some(Vec::new()));
582 }
583
584 #[test]
585 fn normal_build_in_steps_matches_compute_all() {
586 let (mut scene, id) = scene_with(UpdateMode::Eager);
589 scene
590 .edit(id, |tx| {
591 tx.remove_triangles(&[0, 1, 7, 30])?;
592 tx.add_vertices(&[[9.0, 9.0, 9.0]])
593 })
594 .unwrap();
595 let mesh = scene.mesh(id).unwrap();
596 let want = super::NormalState::new(mesh).unwrap();
597 let bits =
598 |n: &[[f32; 3]]| -> Vec<u32> { n.iter().flatten().map(|x| x.to_bits()).collect() };
599 for budget in [1, 2, 7, 100, usize::MAX] {
600 let mut build = super::NormalBuild::new();
601 let mut steps = 0;
602 while !build.step(mesh, budget).unwrap() {
603 steps += 1;
604 }
605 if budget == 1 {
606 assert!(steps > mesh.triangle_slots() / 8, "{steps}");
607 }
608 assert!(build.step(mesh, budget).unwrap(), "stays done");
609 let got = build.finish().unwrap();
610 assert_eq!(bits(got.normals()), bits(want.normals()), "budget {budget}");
611 assert_eq!(got.triangles, want.triangles);
612 assert_eq!(got.pending, Some(fast_mesh_store::ChangeSet::default()));
613 }
614 let mut build = super::NormalBuild::new();
616 build.step(mesh, 1).unwrap();
617 assert!(build.finish().is_none());
618 }
619
620 #[test]
621 fn content_stamp_moves_on_every_change_of_the_render_buffers() {
622 let (mut scene, id) = scene_with(UpdateMode::Eager);
626 let mut last = scene.content_stamp(id).unwrap();
627 let mut moved = |scene: &Scene, what: &str, want: bool| {
628 let now = scene.content_stamp(id).unwrap();
629 assert!(now >= last, "{what}: went down");
630 assert_eq!(now > last, want, "{what}");
631 last = now;
632 };
633 scene.set_normals(id, true).unwrap();
634 moved(&scene, "normals on", true);
635 move_by(&mut scene, id, 5, [0.0, 0.0, 0.5]);
636 moved(&scene, "edit", true);
637 scene.undo().unwrap();
638 moved(&scene, "undo", true);
639 scene.redo().unwrap();
640 moved(&scene, "redo", true);
641 scene.edit(id, |tx| tx.remove_triangles(&[3])).unwrap();
642 scene.compact(id).unwrap();
643 moved(&scene, "remove and compact", true);
644 let mut p = scene.placement(id).unwrap();
645 p.translation = [2.0, 0.0, 0.0];
646 scene.set_placement(id, p).unwrap();
647 moved(&scene, "placement", false);
648 scene.set_normals(id, false).unwrap();
649 moved(&scene, "normals off", true);
650 scene.remove_mesh(id).unwrap();
651 assert_eq!(scene.content_stamp(id), Err(SceneError::UnknownMesh));
652 }
653
654 #[test]
655 fn normals_off_by_default_and_none() {
656 let (mut scene, id) = scene_with(UpdateMode::Eager);
657 assert_eq!(scene.normals(id), Ok(None));
658 assert_eq!(normals_dirty(&mut scene, id), None, "added");
659 move_by(&mut scene, id, 5, [0.0, 0.0, 1.0]);
660 assert_eq!(normals_dirty(&mut scene, id), Some(Vec::new()), "off");
661 scene.set_normals(id, true).unwrap();
662 assert_eq!(normals_dirty(&mut scene, id), None);
663 scene.set_normals(id, true).unwrap();
664 assert!(scene.take_changes().is_empty(), "no change");
665 scene.set_normals(id, false).unwrap();
666 assert_eq!(scene.normals(id), Ok(None));
667 assert_eq!(normals_dirty(&mut scene, id), None);
668 scene.remove_mesh(id).unwrap();
669 assert_eq!(scene.set_normals(id, true), Err(SceneError::UnknownMesh));
670 assert_eq!(scene.normals(id), Err(SceneError::UnknownMesh));
671 }
672
673 #[test]
674 fn normals_after_compact_all() {
675 for mode in [UpdateMode::Eager, UpdateMode::Lazy, UpdateMode::Manual] {
676 let (mut scene, id) = scene_with(mode);
677 scene.set_normals(id, true).unwrap();
678 let mesh = scene.mesh(id).unwrap();
679 let doomed: Vec<TriangleId> = (0..mesh.triangle_slots() as TriangleId)
680 .filter(|&t| mesh.triangle(t).unwrap().contains(&0))
681 .collect();
682 scene
683 .edit(id, |tx| {
684 tx.remove_triangles(&doomed)?;
685 tx.remove_vertices(&[0])
686 })
687 .unwrap();
688 scene.take_changes();
689 scene.compact(id).unwrap();
690 assert_eq!(scene.mesh(id).unwrap().vertex_slots(), 48);
691 assert_eq!(normals_dirty(&mut scene, id), None, "{mode:?}");
692 assert_matches(&scene, id);
693 }
694 }
695
696 #[test]
697 fn lazy_normals_update_on_flush() {
698 let (mut scene, id) = scene_with(UpdateMode::Lazy);
699 scene.set_normals(id, true).unwrap();
700 scene.take_changes();
701 let before = scene.normals(id).unwrap().unwrap().to_vec();
702 move_by(&mut scene, id, 10, [0.0, 0.0, 0.8]);
703 let added = scene
704 .edit(id, |tx| {
705 let v = tx.add_vertices(&[[0.5, 0.5, 2.0], [9.0, 9.0, 9.0]])?;
706 tx.add_triangles(&[[0, 1, v[0]]])?;
707 Ok::<_, SceneError>(v)
708 })
709 .unwrap();
710 let slots = scene.mesh(id).unwrap().vertex_slots();
711 let now = scene.normals(id).unwrap().unwrap();
712 assert_eq!(now.len(), slots, "grown with the slots");
713 assert_eq!(&now[..before.len()], &before[..], "stale until flushed");
714 assert_eq!(normals_dirty(&mut scene, id), Some(Vec::new()));
715 scene.flush().unwrap();
716 assert_matches(&scene, id);
717 let mesh = scene.mesh(id).unwrap();
718 let mut want = ring_of_ring(mesh, 10);
719 want.extend([0, 1, added[0], added[1]]);
720 want.sort_unstable();
721 want.dedup();
722 assert_eq!(normals_dirty(&mut scene, id), Some(want));
723 assert_eq!(
724 scene.normals(id).unwrap().unwrap()[added[1] as usize],
725 [0.0; 3]
726 );
727
728 scene.set_mode(id, UpdateMode::Manual).unwrap();
730 move_by(&mut scene, id, 20, [0.0, 0.3, 0.0]);
731 scene.flush().unwrap();
732 assert_eq!(normals_dirty(&mut scene, id), Some(Vec::new()));
733 scene.update(id).unwrap();
734 assert_eq!(
735 normals_dirty(&mut scene, id),
736 Some(ring_of_ring(scene.mesh(id).unwrap(), 20))
737 );
738 assert_matches(&scene, id);
739 move_by(&mut scene, id, 30, [0.2, 0.0, 0.0]);
741 scene.set_mode(id, UpdateMode::Eager).unwrap();
742 assert_matches(&scene, id);
743 }
744
745 #[test]
746 fn dense_normals_recompute_all_and_report_all() {
747 let (mut scene, id) = scene_with(UpdateMode::Eager);
748 scene.set_normals(id, true).unwrap();
749 scene.take_changes();
750 let mesh = scene.mesh(id).unwrap();
753 let all: Vec<VertexId> = (0..mesh.vertex_slots() as VertexId)
754 .filter(|&v| mesh.is_vertex_alive(v))
755 .collect();
756 assert!(all.len() > 1);
757 scene
758 .edit(id, |tx| {
759 let spots: Vec<[f32; 3]> = all
760 .iter()
761 .map(|&v| {
762 let p = tx.mesh().position(v).unwrap();
763 [p[0], p[1], p[2] + 1.0]
764 })
765 .collect();
766 tx.set_positions(&all, &spots)
767 })
768 .unwrap();
769 assert_eq!(normals_dirty(&mut scene, id), None);
770 assert_matches(&scene, id);
771 }
772
773 #[test]
774 fn dense_triangle_removal_reports_all() {
775 let (mut scene, id) = scene_with(UpdateMode::Eager);
776 scene.set_normals(id, true).unwrap();
777 scene.take_changes();
778 let mesh = scene.mesh(id).unwrap();
781 let doomed: Vec<TriangleId> = (0..mesh.triangle_slots() as TriangleId)
782 .filter(|&t| mesh.is_triangle_alive(t))
783 .take(mesh.triangle_slots() / 2 + 1)
784 .collect();
785 assert!(doomed.len() > 1);
786 scene.edit(id, |tx| tx.remove_triangles(&doomed)).unwrap();
787 assert_eq!(normals_dirty(&mut scene, id), None);
788 assert_matches(&scene, id);
789 }
790}