cadforge_core/
property.rs1use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub enum PropertyValue {
17 Text(String),
18 Integer(i64),
19 Real(f64),
20 Boolean(bool),
21 Length(f64),
24 Area(f64),
26 Volume(f64),
28 Count(i64),
30}
31
32impl PropertyValue {
33 pub fn ifc_type(&self) -> &'static str {
35 match self {
36 Self::Text(_) => "IfcText",
37 Self::Integer(_) => "IfcInteger",
38 Self::Real(_) => "IfcReal",
39 Self::Boolean(_) => "IfcBoolean",
40 Self::Length(_) => "IfcLengthMeasure",
41 Self::Area(_) => "IfcAreaMeasure",
42 Self::Volume(_) => "IfcVolumeMeasure",
43 Self::Count(_) => "IfcCountMeasure",
44 }
45 }
46
47 pub fn as_f64(&self) -> Option<f64> {
49 match *self {
50 Self::Integer(v) | Self::Count(v) => Some(v as f64),
51 Self::Real(v) | Self::Length(v) | Self::Area(v) | Self::Volume(v) => Some(v),
52 Self::Text(_) | Self::Boolean(_) => None,
53 }
54 }
55}
56
57#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
59pub struct PropertySet {
60 pub properties: BTreeMap<String, PropertyValue>,
61}
62
63#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
65pub struct PropertySets {
66 sets: BTreeMap<String, PropertySet>,
67}
68
69impl PropertySets {
70 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn get(&self, set: &str, name: &str) -> Option<&PropertyValue> {
75 self.sets.get(set)?.properties.get(name)
76 }
77
78 pub fn set(
83 &mut self,
84 set: &str,
85 name: &str,
86 value: Option<PropertyValue>,
87 ) -> Option<PropertyValue> {
88 match value {
89 Some(v) => self
90 .sets
91 .entry(set.to_owned())
92 .or_default()
93 .properties
94 .insert(name.to_owned(), v),
95 None => {
96 let entry = self.sets.get_mut(set)?;
97 let previous = entry.properties.remove(name);
98 if entry.properties.is_empty() {
99 self.sets.remove(set);
100 }
101 previous
102 }
103 }
104 }
105
106 pub fn set_names(&self) -> impl Iterator<Item = &str> {
107 self.sets.keys().map(String::as_str)
108 }
109
110 pub fn iter(&self) -> impl Iterator<Item = (&str, &PropertySet)> {
111 self.sets.iter().map(|(k, v)| (k.as_str(), v))
112 }
113
114 pub fn is_empty(&self) -> bool {
115 self.sets.is_empty()
116 }
117
118 pub fn len(&self) -> usize {
119 self.sets.values().map(|s| s.properties.len()).sum()
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn set_then_unset_leaves_no_trace() {
129 let mut p = PropertySets::new();
130 assert!(p
131 .set(
132 "Pset_WallCommon",
133 "IsExternal",
134 Some(PropertyValue::Boolean(true))
135 )
136 .is_none());
137 assert_eq!(
138 p.get("Pset_WallCommon", "IsExternal"),
139 Some(&PropertyValue::Boolean(true))
140 );
141
142 let previous = p.set("Pset_WallCommon", "IsExternal", None);
143 assert_eq!(previous, Some(PropertyValue::Boolean(true)));
144 assert_eq!(p, PropertySets::new());
146 }
147
148 #[test]
149 fn set_returns_the_previous_value_for_inversion() {
150 let mut p = PropertySets::new();
151 p.set(
152 "Pset_WallCommon",
153 "FireRating",
154 Some(PropertyValue::Text("60".into())),
155 );
156 let previous = p.set(
157 "Pset_WallCommon",
158 "FireRating",
159 Some(PropertyValue::Text("90".into())),
160 );
161 assert_eq!(previous, Some(PropertyValue::Text("60".into())));
162 }
163
164 #[test]
165 fn measures_keep_their_ifc_type() {
166 assert_eq!(PropertyValue::Length(2.4).ifc_type(), "IfcLengthMeasure");
167 assert_eq!(PropertyValue::Real(2.4).ifc_type(), "IfcReal");
168 assert_ne!(PropertyValue::Length(2.4), PropertyValue::Real(2.4));
170 assert_eq!(PropertyValue::Length(2.4).as_f64(), Some(2.4));
171 assert_eq!(PropertyValue::Text("x".into()).as_f64(), None);
172 }
173
174 #[test]
175 fn ordering_is_stable_for_reproducible_export() {
176 let mut a = PropertySets::new();
177 a.set("B", "two", Some(PropertyValue::Integer(2)));
178 a.set("A", "one", Some(PropertyValue::Integer(1)));
179
180 let mut b = PropertySets::new();
181 b.set("A", "one", Some(PropertyValue::Integer(1)));
182 b.set("B", "two", Some(PropertyValue::Integer(2)));
183
184 assert_eq!(a.set_names().collect::<Vec<_>>(), ["A", "B"]);
185 assert_eq!(a, b, "insertion order must not affect the model");
186 }
187}