visualization/
material_colors.rs1use bevy::prelude::*;
3use fem_core::{AnalysisSetup, ElementId, FemMesh};
4use std::collections::{BTreeMap, BTreeSet};
5
6#[derive(Resource, Default, Clone, Copy, Debug, PartialEq, Eq)]
7pub enum MaterialColorMode {
8 Part,
9 #[default]
10 Material,
11}
12
13#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
14pub(crate) enum MaterialIdentity {
15 Unassigned,
16 Invalid,
17 Assigned(String),
18}
19pub const UNASSIGNED_MATERIAL_COLOR: Color = Color::srgb(0.48, 0.51, 0.55);
20pub const INVALID_MATERIAL_COLOR: Color = Color::srgb(0.95, 0.16, 0.65);
21
22pub fn material_identity_color(name: &str) -> Color {
23 let hash = name.bytes().fold(2166136261u32, |hash, byte| {
26 (hash ^ byte as u32).wrapping_mul(16777619)
27 });
28 Color::hsl((hash as f64 / u32::MAX as f64 * 360.0) as f32, 0.48, 0.58)
29}
30impl MaterialIdentity {
31 pub(crate) fn color(&self) -> Color {
32 match self {
33 Self::Unassigned => UNASSIGNED_MATERIAL_COLOR,
34 Self::Invalid => INVALID_MATERIAL_COLOR,
35 Self::Assigned(name) => material_identity_color(name),
36 }
37 }
38}
39
40pub(crate) fn resolve_materials(
41 setup: &AnalysisSetup,
42 mesh_index: usize,
43 mesh: &FemMesh,
44) -> BTreeMap<ElementId, MaterialIdentity> {
45 let sections = setup.build_element_section_map(mesh_index, mesh);
46 let mut names = BTreeMap::<&str, usize>::new();
47 for material in &setup.materials {
48 *names.entry(&material.name).or_default() += 1;
49 }
50 let mut colors: BTreeMap<_, _> = mesh
51 .elements
52 .iter()
53 .map(|element| {
54 let identity = match sections.get(&element.id) {
55 None => MaterialIdentity::Unassigned,
56 Some(section) if names.get(section.material_name.as_str()) == Some(&1) => {
57 MaterialIdentity::Assigned(section.material_name.clone())
58 }
59 _ => MaterialIdentity::Invalid,
60 };
61 (element.id, identity)
62 })
63 .collect();
64 let mut owners: BTreeMap<ElementId, BTreeSet<&str>> = BTreeMap::new();
67 for section in setup.sections_for_mesh(mesh_index) {
68 if let Some(group) = section
69 .element_set_name
70 .as_ref()
71 .and_then(|name| mesh.element_sets.iter().find(|group| &group.name == name))
72 {
73 for id in &group.elements {
74 owners
75 .entry(*id)
76 .or_default()
77 .insert(§ion.material_name);
78 }
79 }
80 }
81 for (id, owners) in owners {
82 if owners.len() > 1 && colors.contains_key(&id) {
83 colors.insert(id, MaterialIdentity::Invalid);
84 }
85 }
86 colors
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use fem_core::{FemElementSet, SectionKind};
93 #[test]
94 fn identity_is_stable_and_not_based_on_part_number() {
95 assert_eq!(
96 material_identity_color("STEEL"),
97 material_identity_color("STEEL")
98 );
99 assert_ne!(
100 material_identity_color("STEEL"),
101 material_identity_color("AL6082")
102 );
103 let mesh = FemMesh::demo_hex8();
104 let mut setup = AnalysisSetup::default();
105 setup.add_material("STEEL", Some(210e9), Some(0.3), Some(7850.0));
106 for index in [0, 1] {
107 setup.add_section(index, "STEEL", None, SectionKind::Solid);
108 }
109 assert_eq!(
110 resolve_materials(&setup, 0, &mesh),
111 resolve_materials(&setup, 1, &mesh)
112 );
113 assert!(
114 resolve_materials(&setup, 2, &mesh)
115 .values()
116 .all(|color| *color == MaterialIdentity::Unassigned)
117 );
118 }
119 #[test]
120 fn group_precedence_missing_materials_and_conflicts_are_explicit() {
121 let mut mesh = FemMesh::demo_hex8();
122 let mut setup = AnalysisSetup::default();
123 mesh.element_sets.push(FemElementSet {
124 name: "PATCH".into(),
125 elements: vec![ElementId(0)],
126 });
127 for name in ["A", "B"] {
128 setup.add_material(name, Some(1.0), Some(0.3), None);
129 }
130 setup.add_section(0, "A", None, SectionKind::Solid);
131 setup.add_section(0, "B", Some("PATCH".into()), SectionKind::Solid);
132 assert_eq!(
133 resolve_materials(&setup, 0, &mesh)[&ElementId(0)],
134 MaterialIdentity::Assigned("B".into())
135 );
136 setup.add_section(0, "A", Some("PATCH".into()), SectionKind::Solid);
137 assert_eq!(
138 resolve_materials(&setup, 0, &mesh)[&ElementId(0)],
139 MaterialIdentity::Invalid
140 );
141 setup.sections.truncate(1);
142 setup.materials.clear();
143 assert_eq!(
144 resolve_materials(&setup, 0, &mesh)[&ElementId(0)],
145 MaterialIdentity::Invalid
146 );
147 }
148}