1use crate::math::Fix128;
85use crate::sdf_collider::{fd_normal, SdfField, FD_NORMAL_BASE_EPS};
86use crate::sim_field::ScalarField3D;
87use crate::world_participant::{ObservationSink, StateError};
88
89#[cfg(not(feature = "std"))]
90use alloc::boxed::Box;
91#[cfg(not(feature = "std"))]
92use alloc::vec::Vec;
93
94pub trait PhysicsModifier: Send + Sync {
100 fn modify_distance(&self, x: f32, y: f32, z: f32, original_dist: f32) -> f32;
105
106 fn update(&mut self, dt: f32);
108
109 fn name(&self) -> &str;
111
112 fn is_active(&self) -> bool {
114 true
115 }
116}
117
118pub struct ModifiedSdf {
126 original: Box<dyn SdfField>,
128 modifiers: Vec<Box<dyn PhysicsModifier>>,
130 normal_eps: f32,
132}
133
134impl ModifiedSdf {
135 #[must_use]
137 pub fn new(original: Box<dyn SdfField>) -> Self {
138 Self {
139 original,
140 modifiers: Vec::new(),
141 normal_eps: FD_NORMAL_BASE_EPS,
142 }
143 }
144
145 #[must_use]
147 pub fn with_modifier(mut self, modifier: Box<dyn PhysicsModifier>) -> Self {
148 self.modifiers.push(modifier);
149 self
150 }
151
152 pub fn add_modifier(&mut self, modifier: Box<dyn PhysicsModifier>) {
154 self.modifiers.push(modifier);
155 }
156
157 pub fn clear_modifiers(&mut self) {
159 self.modifiers.clear();
160 }
161
162 #[must_use]
164 pub fn modifier_count(&self) -> usize {
165 self.modifiers.len()
166 }
167
168 pub fn update(&mut self, dt: f32) {
170 for m in &mut self.modifiers {
171 m.update(dt);
172 }
173 }
174
175 #[allow(clippy::option_if_let_else)]
177 pub fn modifier_mut(&mut self, index: usize) -> Option<&mut dyn PhysicsModifier> {
178 match self.modifiers.get_mut(index) {
179 Some(m) => Some(m.as_mut()),
180 None => None,
181 }
182 }
183
184 #[inline]
186 fn eval_distance(&self, x: f32, y: f32, z: f32) -> f32 {
187 let mut d = self.original.distance(x, y, z);
188 for m in &self.modifiers {
189 if m.is_active() {
190 d = m.modify_distance(x, y, z, d);
191 }
192 }
193 d
194 }
195}
196
197impl SdfField for ModifiedSdf {
198 #[inline]
199 fn distance(&self, x: f32, y: f32, z: f32) -> f32 {
200 self.eval_distance(x, y, z)
201 }
202
203 #[inline]
204 fn normal(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
205 fd_normal(
206 |a, b, c| self.eval_distance(a, b, c),
207 self.normal_eps,
208 x,
209 y,
210 z,
211 )
212 }
213
214 fn distance_and_normal(&self, x: f32, y: f32, z: f32) -> (f32, (f32, f32, f32)) {
215 let d = self.eval_distance(x, y, z);
216 let n = self.normal(x, y, z);
217 (d, n)
218 }
219}
220
221pub struct SingleModifiedSdf<M: PhysicsModifier> {
232 pub original: Box<dyn SdfField>,
234 pub modifier: M,
236 normal_eps: f32,
238}
239
240impl<M: PhysicsModifier> SingleModifiedSdf<M> {
241 pub fn new(original: Box<dyn SdfField>, modifier: M) -> Self {
243 Self {
244 original,
245 modifier,
246 normal_eps: FD_NORMAL_BASE_EPS,
247 }
248 }
249
250 pub fn update(&mut self, dt: f32) {
252 self.modifier.update(dt);
253 }
254
255 #[inline]
258 fn eval_distance(&self, x: f32, y: f32, z: f32) -> f32 {
259 let d = self.original.distance(x, y, z);
260 if self.modifier.is_active() {
261 self.modifier.modify_distance(x, y, z, d)
262 } else {
263 d
264 }
265 }
266}
267
268impl<M: PhysicsModifier> SdfField for SingleModifiedSdf<M> {
269 #[inline]
270 fn distance(&self, x: f32, y: f32, z: f32) -> f32 {
271 self.eval_distance(x, y, z)
272 }
273
274 #[inline]
275 fn normal(&self, x: f32, y: f32, z: f32) -> (f32, f32, f32) {
276 fd_normal(
277 |a, b, c| self.eval_distance(a, b, c),
278 self.normal_eps,
279 x,
280 y,
281 z,
282 )
283 }
284
285 fn distance_and_normal(&self, x: f32, y: f32, z: f32) -> (f32, (f32, f32, f32)) {
286 let d = self.eval_distance(x, y, z);
287 let n = self.normal(x, y, z);
288 (d, n)
289 }
290}
291
292pub(crate) const MODIFIER_STATE_VERSION: u32 = 1;
298
299pub(crate) struct StateWriter<'a> {
301 out: &'a mut Vec<u8>,
302}
303
304impl<'a> StateWriter<'a> {
305 pub(crate) fn new(out: &'a mut Vec<u8>) -> Self {
307 out.extend_from_slice(&MODIFIER_STATE_VERSION.to_le_bytes());
308 Self { out }
309 }
310
311 pub(crate) fn u8(&mut self, v: u8) {
312 self.out.push(v);
313 }
314
315 pub(crate) fn bool(&mut self, v: bool) {
316 self.out.push(u8::from(v));
317 }
318
319 pub(crate) fn u64(&mut self, v: u64) {
320 self.out.extend_from_slice(&v.to_le_bytes());
321 }
322
323 pub(crate) fn usize(&mut self, v: usize) {
324 self.u64(v as u64);
325 }
326
327 pub(crate) fn f32(&mut self, v: f32) {
328 self.out.extend_from_slice(&v.to_bits().to_le_bytes());
329 }
330
331 pub(crate) fn vec3(&mut self, v: (f32, f32, f32)) {
332 self.f32(v.0);
333 self.f32(v.1);
334 self.f32(v.2);
335 }
336
337 pub(crate) fn field(&mut self, f: &ScalarField3D) {
339 self.usize(f.nx);
340 self.usize(f.ny);
341 self.usize(f.nz);
342 self.vec3(f.min);
343 self.vec3(f.max);
344 for &v in &f.data {
345 self.f32(v);
346 }
347 }
348}
349
350pub(crate) struct StateReader<'a> {
354 bytes: &'a [u8],
355 at: usize,
356}
357
358impl<'a> StateReader<'a> {
359 pub(crate) fn new(bytes: &'a [u8]) -> Result<Self, StateError> {
362 let mut r = Self { bytes, at: 0 };
363 let mut v = [0_u8; 4];
364 v.copy_from_slice(r.take(4)?);
365 if u32::from_le_bytes(v) != MODIFIER_STATE_VERSION {
366 return Err(StateError::InvalidValue);
367 }
368 Ok(r)
369 }
370
371 fn need(&self, n: usize) -> Result<(), StateError> {
372 let end = self.at.checked_add(n).ok_or(StateError::InvalidValue)?;
373 if end > self.bytes.len() {
374 return Err(StateError::Length {
375 expected: end,
376 found: self.bytes.len(),
377 });
378 }
379 Ok(())
380 }
381
382 fn take(&mut self, n: usize) -> Result<&'a [u8], StateError> {
383 self.need(n)?;
384 let s = &self.bytes[self.at..self.at + n];
385 self.at += n;
386 Ok(s)
387 }
388
389 pub(crate) fn u8(&mut self) -> Result<u8, StateError> {
390 Ok(self.take(1)?[0])
391 }
392
393 pub(crate) fn bool(&mut self) -> Result<bool, StateError> {
394 match self.u8()? {
395 0 => Ok(false),
396 1 => Ok(true),
397 _ => Err(StateError::InvalidValue),
398 }
399 }
400
401 pub(crate) fn u64(&mut self) -> Result<u64, StateError> {
402 let mut v = [0_u8; 8];
403 v.copy_from_slice(self.take(8)?);
404 Ok(u64::from_le_bytes(v))
405 }
406
407 pub(crate) fn usize(&mut self) -> Result<usize, StateError> {
408 usize::try_from(self.u64()?).map_err(|_| StateError::InvalidValue)
409 }
410
411 pub(crate) fn f32(&mut self) -> Result<f32, StateError> {
412 let mut v = [0_u8; 4];
413 v.copy_from_slice(self.take(4)?);
414 Ok(f32::from_bits(u32::from_le_bytes(v)))
415 }
416
417 pub(crate) fn vec3(&mut self) -> Result<(f32, f32, f32), StateError> {
418 Ok((self.f32()?, self.f32()?, self.f32()?))
419 }
420
421 pub(crate) fn count(&mut self, min_item_bytes: usize) -> Result<usize, StateError> {
424 let n = self.usize()?;
425 self.need(
426 n.checked_mul(min_item_bytes)
427 .ok_or(StateError::InvalidValue)?,
428 )?;
429 Ok(n)
430 }
431
432 pub(crate) fn field(&mut self) -> Result<ScalarField3D, StateError> {
435 let nx = self.usize()?;
436 let ny = self.usize()?;
437 let nz = self.usize()?;
438 let min = self.vec3()?;
439 let max = self.vec3()?;
440 let cells = nx
441 .checked_mul(ny)
442 .and_then(|c| c.checked_mul(nz))
443 .ok_or(StateError::InvalidValue)?;
444 self.need(cells.checked_mul(4).ok_or(StateError::InvalidValue)?)?;
445 let mut f = ScalarField3D::new(nx, ny, nz, min, max);
446 for v in &mut f.data {
447 *v = self.f32()?;
448 }
449 Ok(f)
450 }
451
452 pub(crate) fn finish(self) -> Result<(), StateError> {
454 if self.at == self.bytes.len() {
455 Ok(())
456 } else {
457 Err(StateError::Length {
458 expected: self.at,
459 found: self.bytes.len(),
460 })
461 }
462 }
463}
464
465pub(crate) fn observe_max(out: &mut ObservationSink, channel: u32, f: &ScalarField3D) {
468 let max = f
469 .data
470 .iter()
471 .copied()
472 .filter(|v| !v.is_nan())
473 .reduce(f32::max);
474 if let Some(m) = max {
475 out.push(channel, Fix128::from_f32(m));
476 }
477}
478
479pub(crate) fn observe_sum(out: &mut ObservationSink, channel: u32, f: &ScalarField3D) {
483 let sum = f
484 .data
485 .iter()
486 .fold(Fix128::ZERO, |acc, &v| acc + Fix128::from_f32(v));
487 out.push(channel, sum);
488}
489
490#[cfg(test)]
495mod tests {
496 use super::*;
497 use crate::sdf_collider::ClosureSdf;
498
499 struct ExpandModifier {
501 amount: f32,
502 }
503
504 impl PhysicsModifier for ExpandModifier {
505 fn modify_distance(&self, _x: f32, _y: f32, _z: f32, d: f32) -> f32 {
506 d - self.amount
507 }
508 fn update(&mut self, _dt: f32) {}
509 fn name(&self) -> &'static str {
510 "expand"
511 }
512 }
513
514 #[test]
515 fn test_modified_sdf_no_modifiers() {
516 let sphere = ClosureSdf::new(
517 |x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
518 |x, y, z| {
519 let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
520 if len < 1e-10 {
521 (0.0, 1.0, 0.0)
522 } else {
523 (x / len, y / len, z / len)
524 }
525 },
526 );
527
528 let modified = ModifiedSdf::new(Box::new(sphere));
529 let d = modified.distance(2.0, 0.0, 0.0);
530 assert!(
531 (d - 1.0).abs() < 0.01,
532 "No modifiers should pass through, got {d}"
533 );
534 }
535
536 #[test]
537 fn test_modified_sdf_expand() {
538 let sphere = ClosureSdf::new(
539 |x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
540 |x, y, z| {
541 let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
542 if len < 1e-10 {
543 (0.0, 1.0, 0.0)
544 } else {
545 (x / len, y / len, z / len)
546 }
547 },
548 );
549
550 let modified = ModifiedSdf::new(Box::new(sphere))
551 .with_modifier(Box::new(ExpandModifier { amount: 0.5 }));
552
553 let d = modified.distance(2.0, 0.0, 0.0);
556 assert!(
557 (d - 0.5).abs() < 0.01,
558 "Expand should reduce distance, got {d}"
559 );
560 }
561
562 #[test]
563 fn test_modifier_chain() {
564 let sphere = ClosureSdf::new(
565 |x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
566 |x, y, z| {
567 let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
568 if len < 1e-10 {
569 (0.0, 1.0, 0.0)
570 } else {
571 (x / len, y / len, z / len)
572 }
573 },
574 );
575
576 let modified = ModifiedSdf::new(Box::new(sphere))
577 .with_modifier(Box::new(ExpandModifier { amount: 0.3 }))
578 .with_modifier(Box::new(ExpandModifier { amount: 0.2 }));
579
580 let d = modified.distance(2.0, 0.0, 0.0);
582 assert!(
583 (d - 0.5).abs() < 0.01,
584 "Chain should sum expansions, got {d}"
585 );
586 }
587
588 #[test]
589 fn test_single_modified_sdf() {
590 let sphere = ClosureSdf::new(
591 |x, y, z| z.mul_add(z, x.mul_add(x, y * y)).sqrt() - 1.0,
592 |x, y, z| {
593 let len = z.mul_add(z, x.mul_add(x, y * y)).sqrt();
594 if len < 1e-10 {
595 (0.0, 1.0, 0.0)
596 } else {
597 (x / len, y / len, z / len)
598 }
599 },
600 );
601
602 let modified = SingleModifiedSdf::new(Box::new(sphere), ExpandModifier { amount: 0.5 });
603
604 let d = modified.distance(2.0, 0.0, 0.0);
605 assert!((d - 0.5).abs() < 0.01, "Single modifier expand, got {d}");
606 }
607
608 fn ground() -> ClosureSdf {
610 ClosureSdf::new(|_x, y, _z| y, |_x, _y, _z| (0.0, 1.0, 0.0))
611 }
612
613 struct GrowModifier {
615 amount: f32,
616 }
617
618 impl PhysicsModifier for GrowModifier {
619 fn modify_distance(&self, _x: f32, _y: f32, _z: f32, d: f32) -> f32 {
620 d - self.amount
621 }
622 fn update(&mut self, dt: f32) {
623 self.amount += dt;
624 }
625 fn name(&self) -> &'static str {
626 "grow"
627 }
628 }
629
630 #[test]
631 fn clear_modifiers_restores_original_field_and_allows_re_adding() {
632 let mut modified = ModifiedSdf::new(Box::new(ground()))
633 .with_modifier(Box::new(ExpandModifier { amount: 0.25 }))
634 .with_modifier(Box::new(ExpandModifier { amount: 0.5 }));
635 assert_eq!(modified.modifier_count(), 2);
636 assert!((modified.distance(0.0, 2.0, 0.0) - 1.25).abs() < 1e-6);
638
639 modified.clear_modifiers();
640 assert_eq!(modified.modifier_count(), 0);
641 assert!(modified.modifier_mut(0).is_none());
642 assert!((modified.distance(0.0, 2.0, 0.0) - 2.0).abs() < 1e-6);
644 let (nx, ny, nz) = modified.normal(0.0, 2.0, 0.0);
645 assert!(nx.abs() < 1e-6 && (ny - 1.0).abs() < 1e-6 && nz.abs() < 1e-6);
646 modified.clear_modifiers();
648 modified.add_modifier(Box::new(ExpandModifier { amount: 1.0 }));
649 assert_eq!(modified.modifier_count(), 1);
650 assert!((modified.distance(0.0, 2.0, 0.0) - 1.0).abs() < 1e-6);
651 }
652
653 #[test]
654 fn modifier_mut_gives_in_place_access_by_index() {
655 let mut modified = ModifiedSdf::new(Box::new(ground()))
656 .with_modifier(Box::new(GrowModifier { amount: 0.25 }))
657 .with_modifier(Box::new(ExpandModifier { amount: 0.5 }));
658 assert!((modified.distance(0.0, 2.0, 0.0) - 1.25).abs() < 1e-6);
660
661 assert_eq!(modified.modifier_mut(0).map(|m| m.name()), Some("grow"));
663 assert_eq!(modified.modifier_mut(1).map(|m| m.name()), Some("expand"));
664 assert!(modified.modifier_mut(2).is_none());
665
666 match modified.modifier_mut(0) {
668 Some(m) => m.update(0.5),
669 None => panic!("modifier 0 must exist"),
670 }
671 assert!((modified.distance(0.0, 2.0, 0.0) - 0.75).abs() < 1e-6);
672 match modified.modifier_mut(1) {
674 Some(m) => m.update(0.5),
675 None => panic!("modifier 1 must exist"),
676 }
677 assert!((modified.distance(0.0, 2.0, 0.0) - 0.75).abs() < 1e-6);
678 modified.update(0.25);
680 assert!((modified.distance(0.0, 2.0, 0.0) - 0.5).abs() < 1e-6);
681 assert_eq!(modified.modifier_count(), 2);
682 }
683
684 struct InactiveModifier {
687 amount: f32,
688 updates: u32,
689 }
690
691 impl PhysicsModifier for InactiveModifier {
692 fn modify_distance(&self, _x: f32, _y: f32, _z: f32, d: f32) -> f32 {
693 d - self.amount
694 }
695 fn update(&mut self, _dt: f32) {
696 self.updates += 1;
697 }
698 fn name(&self) -> &'static str {
699 "inactive"
700 }
701 fn is_active(&self) -> bool {
702 false
703 }
704 }
705
706 #[test]
711 fn distance_and_normal_of_both_wrappers_on_the_ground() {
712 let multi = ModifiedSdf::new(Box::new(ground()))
713 .with_modifier(Box::new(ExpandModifier { amount: 0.5 }))
714 .with_modifier(Box::new(InactiveModifier {
715 amount: 9.0,
716 updates: 0,
717 }));
718 let (d, n) = multi.distance_and_normal(0.0, 2.0, 0.0);
719 assert!((d - 1.5).abs() < 1e-6, "{d}");
720 assert!(
721 n.0.abs() < 1e-6 && (n.1 - 1.0).abs() < 1e-6 && n.2.abs() < 1e-6,
722 "{n:?}"
723 );
724
725 let mut grow = SingleModifiedSdf::new(Box::new(ground()), GrowModifier { amount: 0.25 });
726 grow.update(0.5);
727 let (d, n) = grow.distance_and_normal(0.0, 2.0, 0.0);
728 assert!((d - 1.25).abs() < 1e-6, "{d}");
729 assert!(
730 n.0.abs() < 1e-6 && (n.1 - 1.0).abs() < 1e-6 && n.2.abs() < 1e-6,
731 "{n:?}"
732 );
733 let n = grow.normal(1.0, 3.0, -1.0);
734 assert!((n.1 - 1.0).abs() < 1e-6, "{n:?}");
735
736 let mut off = SingleModifiedSdf::new(
737 Box::new(ground()),
738 InactiveModifier {
739 amount: 9.0,
740 updates: 0,
741 },
742 );
743 off.update(0.5);
744 off.update(0.5);
745 assert_eq!(off.modifier.updates, 2);
746 assert!((off.distance(0.0, 2.0, 0.0) - 2.0).abs() < 1e-6);
747 }
748
749 #[test]
754 fn state_writer_byte_layout() {
755 let mut out = Vec::new();
756 let mut w = StateWriter::new(&mut out);
757 w.u8(7);
758 w.bool(true);
759 w.bool(false);
760 w.u64(0x0102_0304_0506_0708);
761 w.usize(5);
762 w.f32(1.5);
763 w.vec3((1.0, -2.0, 0.5));
764 let mut f = ScalarField3D::new(2, 1, 1, (0.0, 0.0, 0.0), (2.0, 1.0, 1.0));
765 f.data = vec![0.25, -4.0];
766 w.field(&f);
767
768 let mut want: Vec<u8> = vec![
769 1, 0, 0, 0, 7, 1, 0, 8, 7, 6, 5, 4, 3, 2, 1, 5, 0, 0, 0, 0, 0, 0, 0,
770 ];
771 for v in [1.5_f32, 1.0, -2.0, 0.5] {
772 want.extend_from_slice(&v.to_bits().to_le_bytes());
773 }
774 for n in [2_u64, 1, 1] {
775 want.extend_from_slice(&n.to_le_bytes());
776 }
777 for v in [0.0_f32, 0.0, 0.0, 2.0, 1.0, 1.0, 0.25, -4.0] {
778 want.extend_from_slice(&v.to_bits().to_le_bytes());
779 }
780 assert_eq!(out, want);
781
782 let mut r = StateReader::new(&out).expect("version 1");
784 assert_eq!(r.u8(), Ok(7));
785 assert_eq!(r.bool(), Ok(true));
786 assert_eq!(r.bool(), Ok(false));
787 assert_eq!(r.u64(), Ok(0x0102_0304_0506_0708));
788 assert_eq!(r.usize(), Ok(5));
789 assert_eq!(r.f32(), Ok(1.5));
790 assert_eq!(r.vec3(), Ok((1.0, -2.0, 0.5)));
791 let g = r.field().expect("field");
792 assert_eq!(
793 (g.nx, g.ny, g.nz, g.min, g.max),
794 (2, 1, 1, (0.0, 0.0, 0.0), (2.0, 1.0, 1.0))
795 );
796 assert_eq!(g.data, vec![0.25, -4.0]);
797 assert_eq!(r.finish(), Ok(()));
798 }
799
800 #[test]
805 fn state_reader_refusals() {
806 assert_eq!(
807 StateReader::new(&[2, 0, 0, 0]).err(),
808 Some(StateError::InvalidValue)
809 );
810 assert_eq!(
811 StateReader::new(&[1, 0]).err(),
812 Some(StateError::Length {
813 expected: 4,
814 found: 2
815 })
816 );
817 let mut r = StateReader::new(&[1, 0, 0, 0, 2]).expect("version");
818 assert_eq!(r.bool(), Err(StateError::InvalidValue));
819 let mut r = StateReader::new(&[1, 0, 0, 0, 9, 9]).expect("version");
820 assert_eq!(
821 r.f32(),
822 Err(StateError::Length {
823 expected: 8,
824 found: 6
825 })
826 );
827
828 let mut bytes = vec![1, 0, 0, 0];
830 bytes.extend_from_slice(&3_u64.to_le_bytes());
831 bytes.extend_from_slice(&[0; 4]);
832 let mut r = StateReader::new(&bytes).expect("version");
833 assert_eq!(
834 r.count(4),
835 Err(StateError::Length {
836 expected: 24,
837 found: 16
838 })
839 );
840 let mut bytes = vec![1, 0, 0, 0];
842 bytes.extend_from_slice(&u64::MAX.to_le_bytes());
843 let mut r = StateReader::new(&bytes).expect("version");
844 assert_eq!(r.count(2), Err(StateError::InvalidValue));
845 let mut r = StateReader::new(&bytes[..]).expect("version");
846 assert_eq!(r.count(1), Err(StateError::InvalidValue));
847
848 let mut bytes = vec![1, 0, 0, 0];
850 for n in [u64::MAX, 2, 1] {
851 bytes.extend_from_slice(&n.to_le_bytes());
852 }
853 bytes.extend_from_slice(&[0; 24]);
854 let mut r = StateReader::new(&bytes).expect("version");
855 assert_eq!(r.field().err(), Some(StateError::InvalidValue));
856
857 let mut r = StateReader::new(&[1, 0, 0, 0, 5, 6]).expect("version");
859 assert_eq!(r.u8(), Ok(5));
860 assert_eq!(
861 r.finish(),
862 Err(StateError::Length {
863 expected: 5,
864 found: 6
865 })
866 );
867 }
868
869 #[test]
873 fn observe_max_and_sum_closed_form() {
874 let mut f = ScalarField3D::new(4, 1, 1, (0.0, 0.0, 0.0), (4.0, 1.0, 1.0));
875 f.data = vec![1.0, f32::NAN, 3.0, -2.0];
876 let mut out = ObservationSink::new();
877 observe_max(&mut out, 4, &f);
878 assert_eq!(out.values(), &[(4, Fix128::from_int(3))]);
879
880 let mut g = ScalarField3D::new(4, 1, 1, (0.0, 0.0, 0.0), (4.0, 1.0, 1.0));
881 g.data = vec![1.0, 0.5, 3.0, -2.0];
882 let mut out = ObservationSink::new();
883 observe_sum(&mut out, 1, &g);
884 assert_eq!(out.values(), &[(1, Fix128::from_ratio(5, 2))]);
885
886 let mut empty = ScalarField3D::new(1, 1, 1, (0.0, 0.0, 0.0), (1.0, 1.0, 1.0));
887 empty.data.clear();
888 let mut out = ObservationSink::new();
889 observe_max(&mut out, 0, &empty);
890 assert!(out.values().is_empty());
891 }
892}