Skip to main content

cadforge_core/
property.rs

1//! Property sets.
2//!
3//! Deliberately `BTreeMap`-backed rather than `HashMap`: export must be byte-reproducible for
4//! a given revision, and golden-file tests depend on stable ordering
5//! (`docs/ifc-semantics.md` §7.2, §12.2).
6
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9
10/// A single property value.
11///
12/// The measure variants carry their IFC measure type because a length and a bare real export
13/// differently and compare differently. Collapsing them to `f64` loses information the
14/// exporter needs.
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub enum PropertyValue {
17    Text(String),
18    Integer(i64),
19    Real(f64),
20    Boolean(bool),
21    /// `IfcLengthMeasure`, in metres. CADForge is metric internally; display units are a UI
22    /// concern (`docs/ifc-semantics.md` §11 Phase 0, units policy).
23    Length(f64),
24    /// `IfcAreaMeasure`, in square metres.
25    Area(f64),
26    /// `IfcVolumeMeasure`, in cubic metres.
27    Volume(f64),
28    /// `IfcCountMeasure`.
29    Count(i64),
30}
31
32impl PropertyValue {
33    /// The IFC measure type name, for export.
34    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    /// Numeric view, where one exists. `None` for text and boolean.
48    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/// One `IfcPropertySet`.
58#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
59pub struct PropertySet {
60    pub properties: BTreeMap<String, PropertyValue>,
61}
62
63/// All property sets on an element, keyed by set name (`Pset_WallCommon`, and so on).
64#[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    /// Set or clear a property. `None` removes it, and removes the set once it is empty so
79    /// that set-then-unset leaves no trace — which is what makes command inversion exact.
80    ///
81    /// Returns the previous value, which is what the inverse command is built from.
82    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        // The empty set is dropped too, so this equals a freshly constructed value.
145        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        // Same number, different meaning — which is exactly why they are distinct variants.
169        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}