ifc_lite_cloud_import/
lib.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/. */
4mod meshes;
5use nalgebra::Matrix4;
6use serde::Deserialize;
7use serde_json::{json, Value};
8use std::collections::{BTreeMap, HashSet};
9
10pub type Result<T> = std::result::Result<T, String>;
11const MAX_OCCURRENCES: usize = 100_000;
12const MAX_DEPTH: usize = 256;
13const MAX_POINTS: usize = 10_000_000;
14
15#[derive(Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct Snapshot {
18    pub revision_id: String,
19    pub elements: BTreeMap<String, Element>,
20    pub site: Value,
21}
22#[derive(Deserialize)]
23pub struct Element {
24    pub urn: String,
25    #[serde(default)]
26    pub properties: Value,
27    #[serde(default)]
28    pub metadata: Value,
29    #[serde(default)]
30    pub representations: BTreeMap<String, Value>,
31    #[serde(default)]
32    pub children: Vec<Child>,
33}
34#[derive(Deserialize)]
35pub struct Child {
36    pub key: String,
37    pub urn: String,
38    pub transform: Option<[f64; 16]>,
39}
40
41pub fn matrix(values: Option<[f64; 16]>) -> Result<Matrix4<f64>> {
42    let m = values.map(|v| Matrix4::from_column_slice(&v)).unwrap_or_else(Matrix4::identity);
43    if !m.iter().all(|v| v.is_finite()) || m[(3, 0)] != 0.0 || m[(3, 1)] != 0.0
44        || m[(3, 2)] != 0.0 || m[(3, 3)] != 1.0 || m.fixed_view::<3, 3>(0, 0).determinant().abs() < 1e-12 {
45        return Err("Invalid or singular affine transform".into());
46    }
47    Ok(m)
48}
49
50/// Immutable URNs cache source data; occurrence paths preserve repeated instances.
51/// Iterative traversal bounds stack, path-scoped cycles AND acyclic DAG fan-out.
52pub fn convert(snapshot: &Snapshot, blobs: &BTreeMap<String, Vec<u8>>) -> Result<Value> {
53    let mut stack = vec![(snapshot.revision_id.clone(), "forma".to_string(), Matrix4::identity(), HashSet::new())];
54    let mut data = Vec::new();
55    let mut point_count = 0;
56    let mut mesh_count = 0;
57    while let Some((urn, path, transform, mut ancestors)) = stack.pop() {
58        if data.len() >= MAX_OCCURRENCES || ancestors.len() >= MAX_DEPTH {
59            return Err("Forma hierarchy exceeds the occurrence/depth limit".into());
60        }
61        if !ancestors.insert(urn.clone()) { return Err("Cycle in Forma element hierarchy".into()); }
62        let element = snapshot.elements.get(&urn).ok_or("Missing referenced Forma element")?;
63        if element.urn != urn { return Err("Forma element revision mismatch".into()); }
64        let mut attrs = json!({
65            "bsi::ifc::class": {"code":"IfcBuildingElementProxy", "uri":"https://identifier.buildingsmart.org/uri/buildingsmart/ifc/5/class/IfcBuildingElementProxy"},
66            "bsi::ifc::prop::Name": element.properties.get("name").and_then(Value::as_str).unwrap_or(&urn),
67            "autodesk::SourceUrn": urn,
68            "autodesk::Properties": element.properties,
69            "autodesk::Metadata": element.metadata,
70            "autodesk::Representations": element.representations,
71        });
72        if path == "forma" {
73            attrs["autodesk::Site"] = snapshot.site.clone();
74            attrs["bsi::ifc::class"] = json!({"code":"IfcSite","uri":"https://identifier.buildingsmart.org/uri/buildingsmart/ifc/5/class/IfcSite"});
75            if let Some(system) = snapshot.site.get("coordinateSystem") {
76                let srid = system["srid"].as_str().map(str::to_string).or_else(|| system["srid"].as_u64().map(|v| v.to_string()));
77                let point = system["refPoint"].as_array();
78                if let (Some(srid), Some(point)) = (srid, point) {
79                    if point.len() >= 2 {
80                        if let (Some(east), Some(north)) = (point[0].as_f64(), point[1].as_f64()) {
81                            let name = if srid.starts_with("EPSG:") { srid } else { format!("EPSG:{srid}") };
82                            attrs["ifclite::georeference::v1"] = json!({
83                                "IfcProjectedCRS":{"Name":name,"MapUnit":"METRE"},
84                                "IfcMapConversion":{"Eastings":east,"Northings":north,"OrthogonalHeight":0,"XAxisAbscissa":1,"XAxisOrdinate":0,"Scale":1}
85                            });
86                        }
87                    }
88                }
89            }
90        }
91        let mut children = BTreeMap::new();
92        let mut keys = HashSet::new();
93        for child in &element.children {
94            if stack.len() + data.len() >= MAX_OCCURRENCES {
95                return Err("Forma hierarchy exceeds the queued occurrence limit".into());
96            }
97            if !keys.insert(&child.key) { return Err("Invalid/duplicate Forma child key".into()); }
98            // Hex UTF-8 avoids delimiter collisions while keeping stable occurrence identities.
99            let key: String = child.key.as_bytes().iter().map(|b| format!("{b:02x}")).collect();
100            let key = format!("key-{key}");
101            let child_path = format!("{path}/{key}");
102            children.insert(key, child_path.clone());
103            stack.push((child.urn.clone(), child_path, transform * matrix(child.transform)?, ancestors.clone()));
104        }
105        let mut node = json!({"path":path, "attributes":attrs, "children":children});
106        if !element.representations.contains_key("volumeMesh")
107            && (element.representations.contains_key("volume25DCollection") || element.representations.contains_key("graphBuilding")) {
108            return Err("Physical Forma representation has no supported volume mesh".into());
109        }
110        if let Some(rep) = element.representations.get("volumeMesh") {
111            if rep["type"] != "linked" { return Err("Unsupported Forma volumeMesh representation".into()); }
112            let blob_id = rep["blobId"].as_str().ok_or("Missing Forma mesh blob")?;
113            let context = element.urn.split(':').nth(3).unwrap_or("");
114            let bytes = blobs.get(&format!("{context}:{blob_id}")).ok_or("Missing Forma mesh data")?;
115            let geometry = meshes::extract(bytes, rep, &transform, &mut point_count)?;
116            if geometry.is_empty() { return Err("Forma mesh selection produced no geometry".into()); }
117            for (index, mesh) in geometry.into_iter().enumerate() {
118                let mesh_path = format!("{path}/mesh-{index}");
119                node["children"][format!("mesh-{index}")] = json!(mesh_path);
120                data.push(json!({"path":mesh_path,"attributes":mesh}));
121                mesh_count += 1;
122            }
123        }
124        data.push(node);
125    }
126    if mesh_count == 0 { return Err("Proposal contains no supported volume meshes".into()); }
127    Ok(json!({
128        "header":{"id":snapshot.revision_id,"ifcxVersion":"ifcx_alpha","dataVersion":snapshot.revision_id,"author":"Autodesk Forma","timestamp":snapshot.elements.get(&snapshot.revision_id).and_then(|e| e.metadata.get("createdAt")).and_then(Value::as_str).unwrap_or("1970-01-01T00:00:00Z")},
129        "imports":[
130            {"uri":"https://ifcx.dev/@standards.buildingsmart.org/ifc/core/ifc@v5a.ifcx"},
131            {"uri":"https://ifcx.dev/@standards.buildingsmart.org/ifc/core/prop@v5a.ifcx"},
132            {"uri":"https://ifcx.dev/@openusd.org/usd@v1.ifcx"}
133        ],"schemas":{},"data":data
134    }))
135}
136
137#[cfg(test)]
138#[path = "tests.rs"]
139mod tests;