Skip to main content

ifc_lite_processing/symbolic/
provenance.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4use super::{SymbolicData, SymbolicFillArea};
5use serde::{Serialize, Serializer, ser::{SerializeSeq, SerializeStruct}};
6
7/// Canonical symbol data plus extraction-bound direct fill identities.
8/// Private fields preserve freedom to extend this result without breaking
9/// callers constructing the legacy `SymbolicData` / `SymbolicFillArea` structs.
10#[derive(Debug, Clone, Default)]
11pub struct SymbolicDataWithProvenance {
12    data: SymbolicData,
13    fill_items: Vec<Option<u32>>,
14}
15impl SymbolicDataWithProvenance {
16    pub(super) fn new(data: SymbolicData, fill_items: Vec<Option<u32>>) -> Self {
17        Self { data, fill_items }
18    }
19    /// Legacy data without modifying any public primitive struct.
20    pub fn data(&self) -> &SymbolicData { &self.data }
21    /// One optional direct item id for each fill, in matching ordinal order.
22    pub fn fill_items(&self) -> &[Option<u32>] { &self.fill_items }
23    /// Consume the result into legacy data and its ordinal-aligned sidecar.
24    pub fn into_parts(self) -> (SymbolicData, Vec<Option<u32>>) { (self.data, self.fill_items) }
25    /// Whether no symbolic primitives were extracted.
26    pub fn is_empty(&self) -> bool { self.data.is_empty() }
27}
28#[derive(Serialize)]
29struct Fill<'a> {
30    #[serde(flatten)]
31    fill: &'a SymbolicFillArea,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    geometry_item_id: Option<u32>,
34}
35struct Fills<'a>(&'a SymbolicDataWithProvenance);
36impl Serialize for Fills<'_> {
37    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
38        let mut seq = serializer.serialize_seq(Some(self.0.data.fills.len()))?;
39        for (ordinal, fill) in self.0.data.fills.iter().enumerate() {
40            seq.serialize_element(&Fill { fill, geometry_item_id: self.0.fill_items.get(ordinal).copied().flatten() })?;
41        }
42        seq.end()
43    }
44}
45impl Serialize for SymbolicDataWithProvenance {
46    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
47        let mut out = serializer.serialize_struct("SymbolicData", 5 + usize::from(self.data.truncated.is_some()))?;
48        out.serialize_field("grid_axes", &self.data.grid_axes)?;
49        out.serialize_field("polylines", &self.data.polylines)?;
50        out.serialize_field("circles", &self.data.circles)?;
51        out.serialize_field("texts", &self.data.texts)?;
52        out.serialize_field("fills", &Fills(self))?;
53        if let Some(truncated) = &self.data.truncated { out.serialize_field("truncated", truncated)?; }
54        out.end()
55    }
56}
57
58// Deserialize the same JSON shape, including old caches with no provenance.
59impl<'de> serde::Deserialize<'de> for SymbolicDataWithProvenance {
60    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
61        #[derive(serde::Deserialize)]
62        struct OwnedFill {
63            #[serde(flatten)]
64            fill: SymbolicFillArea,
65            #[serde(default)]
66            geometry_item_id: Option<u32>,
67        }
68        #[derive(serde::Deserialize)]
69        struct Wire {
70            grid_axes: Vec<super::SymbolicGridAxis>,
71            polylines: Vec<super::SymbolicPolyline>,
72            circles: Vec<super::SymbolicCircle>,
73            texts: Vec<super::SymbolicText>,
74            fills: Vec<OwnedFill>,
75            #[serde(default)]
76            truncated: Option<super::SymbolicTruncation>,
77        }
78        let wire = Wire::deserialize(deserializer)?;
79        let (fills, fill_items) = wire.fills.into_iter().map(|f| (f.fill, f.geometry_item_id)).unzip();
80        Ok(Self { data: SymbolicData { grid_axes: wire.grid_axes, polylines: wire.polylines,
81            circles: wire.circles, texts: wire.texts, fills, truncated: wire.truncated }, fill_items })
82    }
83}
84impl From<SymbolicData> for SymbolicDataWithProvenance {
85    fn from(data: SymbolicData) -> Self {
86        let fill_items = vec![None; data.fills.len()];
87        Self { data, fill_items }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    #[test]
95    fn issue_4459_optional_provenance_preserves_legacy_json_and_nan_sentinels() {
96        let old = r#"{"grid_axes":[],"polylines":[],"circles":[],"texts":[],"fills":[{"express_id":1,"ifc_type":"IfcAnnotation","points":[0,0,1,0,0,1],"holes_offsets":[],"fill_color":[1,0,0,1],"has_hatching":false,"hatch_spacing":0,"hatch_angle":0,"hatch_angle_secondary":null,"hatch_line_width":0,"world_y":null,"representation":"Annotation"}]}"#;
97        let legacy: SymbolicData = serde_json::from_str(old).unwrap();
98        let decoded: SymbolicDataWithProvenance = serde_json::from_str(old).unwrap();
99        assert_eq!(decoded.fill_items(), &[None]);
100        assert!(decoded.data().fills[0].world_y.is_nan());
101        assert_eq!(serde_json::to_value(&decoded).unwrap(), serde_json::to_value(&legacy).unwrap());
102        let mut accumulator = super::super::output_cap::SymbolicAccumulator::new();
103        let mut invalid = legacy.fills[0].clone();
104        invalid.points[0] = f32::NAN;
105        accumulator.push_fill_with_provenance(invalid, Some(77));
106        accumulator.push_fill_with_provenance(legacy.fills[0].clone(), Some(99));
107        let accepted = accumulator.into_provenance();
108        assert_eq!(accepted.data().fills.len(), 1);
109        assert_eq!(accepted.fill_items(), &[Some(99)], "refused appends cannot shift ordinal provenance");
110        let enriched = SymbolicDataWithProvenance::new(legacy, vec![Some(99)]);
111        let bytes = serde_json::to_vec(&enriched).unwrap();
112        let roundtrip: SymbolicDataWithProvenance = serde_json::from_slice(&bytes).unwrap();
113        assert_eq!(roundtrip.fill_items(), &[Some(99)]);
114        let old_reader: SymbolicData = serde_json::from_slice(&bytes).unwrap();
115        assert_eq!(old_reader.fills.len(), 1);
116        assert!(old_reader.fills[0].hatch_angle_secondary.is_nan());
117    }
118}