Skip to main content

visualization/
contour_range.rs

1//! Display ranges use cached field extrema, never rewrite simulation values.
2use bevy::prelude::*;
3use fem_core::{FemResultSet, ResultField};
4
5#[derive(Resource, Default, Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ContourRangeMode {
7    #[default]
8    CurrentFrame,
9    AllFrames,
10}
11
12pub(crate) fn same_kind(a: &ResultField, b: &ResultField) -> bool {
13    std::mem::discriminant(a) == std::mem::discriminant(b)
14}
15
16pub(crate) fn bounds(field: &ResultField) -> Option<(f32, f32)> {
17    let (min, max, has_values) = match field {
18        ResultField::NodeScalar {
19            min, max, values, ..
20        }
21        | ResultField::ElementScalar {
22            min, max, values, ..
23        } => (*min, *max, !values.is_empty()),
24        ResultField::NodeVector {
25            min_mag,
26            max_mag,
27            values,
28            ..
29        } => (*min_mag, *max_mag, !values.is_empty()),
30    };
31    if min == max && field.constant_value().is_none() {
32        return None;
33    }
34    (has_values && min.is_finite() && max.is_finite() && min <= max).then_some((min, max))
35}
36
37pub(crate) fn resolve(results: &FemResultSet, mode: ContourRangeMode) -> Option<(f32, f32)> {
38    let active = results.active.as_ref()?;
39    let selected = results.active_field()?;
40    let mut range: Option<(f32, f32)> = None;
41    for steps in &results.by_mesh {
42        for (index, step) in steps.iter().enumerate() {
43            if mode == ContourRangeMode::CurrentFrame && index != active.step_index {
44                continue;
45            }
46            let Some(field) = step
47                .field_by_name(&active.field_name)
48                .filter(|f| same_kind(f, selected))
49            else {
50                continue;
51            };
52            let Some((min, max)) = bounds(field) else {
53                continue;
54            };
55            range = Some(range.map_or((min, max), |(lo, hi)| (lo.min(min), hi.max(max))));
56        }
57    }
58    range
59}
60
61pub(crate) fn normalize(value: f32, (min, max): (f32, f32)) -> f32 {
62    if min == max {
63        0.5
64    } else {
65        // f64 avoids overflow and preserves meaningful very small ranges.
66        ((value as f64 - min as f64) / (max as f64 - min as f64)).clamp(0.0, 1.0) as f32
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use fem_core::{ActiveResult, StepResult};
74    fn scalar(min: f32, max: f32) -> ResultField {
75        ResultField::NodeScalar {
76            name: "P".into(),
77            values: vec![min, max],
78            min,
79            max,
80        }
81    }
82    #[test]
83    fn all_frames_share_ranges_without_changing_values_or_including_other_fields() {
84        let step = |fields| StepResult {
85            fields,
86            ..default()
87        };
88        let mut r = FemResultSet {
89            by_mesh: vec![
90                vec![step(vec![scalar(0., 1.)]), step(vec![scalar(-5., 3.)])],
91                vec![step(vec![scalar(2., 4.)]), step(vec![scalar(0., 10.)])],
92            ],
93            active: Some(ActiveResult {
94                mesh_index: 0,
95                step_index: 0,
96                field_name: "P".into(),
97            }),
98        };
99        assert_eq!(resolve(&r, ContourRangeMode::CurrentFrame), Some((0., 4.)));
100        assert_eq!(resolve(&r, ContourRangeMode::AllFrames), Some((-5., 10.)));
101        r.active.as_mut().unwrap().step_index = 1;
102        assert_eq!(resolve(&r, ContourRangeMode::AllFrames), Some((-5., 10.)));
103        assert_eq!(bounds(&r.by_mesh[0][0].fields[0]), Some((0., 1.)));
104        r.by_mesh[1][1].fields = vec![ResultField::ElementScalar {
105            name: "P".into(),
106            values: vec![999.],
107            min: 999.,
108            max: 999.,
109        }];
110        assert_eq!(resolve(&r, ContourRangeMode::AllFrames), Some((-5., 4.)));
111        r.by_mesh[0][1].fields.clear();
112        assert_eq!(resolve(&r, ContourRangeMode::AllFrames), None);
113    }
114    #[test]
115    fn constants_tiny_ranges_and_vector_magnitudes_are_not_confused() {
116        assert_eq!(normalize(2., (2., 2.)), 0.5);
117        assert_eq!(normalize(1e-20, (0., 1e-20)), 1.);
118        let vector = ResultField::NodeVector {
119            name: "V".into(),
120            values: vec![Vec3::X],
121            min_mag: 1.,
122            max_mag: 1.,
123        };
124        assert_eq!(bounds(&vector), Some((1., 1.)));
125        assert_eq!(bounds(&scalar(f32::NAN, f32::NAN)), None);
126    }
127
128    #[test]
129    fn fixed_range_maps_constant_frame_to_its_real_color_without_moving_geometry() {
130        use bevy::mesh::VertexAttributeValues;
131        let mesh = fem_core::FemMesh::demo_hex8();
132        let step = StepResult {
133            fields: vec![ResultField::NodeScalar {
134                name: "P".into(),
135                values: vec![0.; 8],
136                min: 0.,
137                max: 0.,
138            }],
139            ..default()
140        };
141        let settings = crate::ContourSettings {
142            mesh_index: 0,
143            step_index: 0,
144            field_name: "P".into(),
145            show_deformation: false,
146            displacement_field: "Displacement".into(),
147            deformation_scale: 1.,
148        };
149        let current =
150            crate::demo_mesh::build_contour_surface_mesh(&mesh, &step, &settings, None).unwrap();
151        let fixed =
152            crate::demo_mesh::build_contour_surface_mesh(&mesh, &step, &settings, Some((0., 10.)))
153                .unwrap();
154        for (surface, t) in [(&current, 0.5), (&fixed, 0.)] {
155            let Some(VertexAttributeValues::Float32x4(colors)) =
156                surface.attribute(Mesh::ATTRIBUTE_COLOR)
157            else {
158                panic!("no colors");
159            };
160            assert!(
161                colors
162                    .iter()
163                    .all(|c| *c == fem_core::rainbow_color(t).to_f32_array())
164            );
165        }
166        let Some(VertexAttributeValues::Float32x3(a)) = current.attribute(Mesh::ATTRIBUTE_POSITION)
167        else {
168            panic!();
169        };
170        let Some(VertexAttributeValues::Float32x3(b)) = fixed.attribute(Mesh::ATTRIBUTE_POSITION)
171        else {
172            panic!();
173        };
174        assert_eq!(a, b);
175    }
176}