1use 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 ((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 [(¤t, 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}