ifc_lite_cloud_import/
lib.rs1mod 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
50pub 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 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;