1use std::collections::{HashMap, HashSet};
9
10use ifc_lite_core::{DecodedEntity, EntityDecoder, EntityScanner};
11use serde_json::{json, Map, Value};
12
13use crate::json::typed_value;
14use crate::model::{build_export_model, EntityRow};
15
16const IFCX_VERSION: &str = "ifcx_alpha";
25
26const IMPORT_CORE: &str = "https://ifcx.dev/@standards.buildingsmart.org/ifc/core/ifc@v5a.ifcx";
27const IMPORT_PROP: &str = "https://ifcx.dev/@standards.buildingsmart.org/ifc/core/prop@v5a.ifcx";
28
29const KNOWN_PROPS: &[&str] = &[
32 "UsageType", "TypeName", "IsExternal", "RefElevation", "ElevationOfRefHeight",
33 "ElevationOfTerrain", "NumberOfStoreys", "Height", "Width", "Length", "Depth",
34 "Volume", "NetVolume", "NetArea", "NetSideArea", "CrossSectionArea", "Station",
35];
36
37pub struct Ifc5Options {
39 pub author: String,
40 pub data_version: String,
41 pub only_known_properties: bool,
43 pub pretty: bool,
44}
45
46impl Default for Ifc5Options {
47 fn default() -> Self {
48 Self {
49 author: "ifc-lite".to_string(),
50 data_version: "1.0.0".to_string(),
51 only_known_properties: true,
52 pretty: false,
53 }
54 }
55}
56
57fn uuid_from_id(id: u32) -> String {
62 let a = (id as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ 0xABCD_EF01_2345_6789;
63 let b = (id as u64).wrapping_mul(0xC2B2_AE3D_27D4_EB4F) ^ 0x0F1E_2D3C_4B5A_6978;
64 let s = format!("{a:016x}{b:016x}");
65 format!("{}-{}-{}-{}-{}", &s[0..8], &s[8..12], &s[12..16], &s[16..20], &s[20..32])
66}
67
68fn path_for_id(id: u32, by_id: &HashMap<u32, &EntityRow>) -> String {
77 if let Some(row) = by_id.get(&id) {
78 if let Some(g) = &row.global_id {
79 if !g.is_empty() {
80 return g.clone();
81 }
82 }
83 }
84 uuid_from_id(id)
85}
86
87fn prim_name(name: &str, fallback_type: &str, id: u32) -> String {
89 let base = if name.trim().is_empty() { fallback_type } else { name };
90 let mut out: String = base
91 .chars()
92 .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
93 .collect();
94 if out.is_empty() || !out.chars().next().unwrap().is_ascii_alphabetic() {
95 out = format!("p_{out}");
96 }
97 format!("{out}_{id}")
99}
100
101pub(crate) fn spatial_children(content: &[u8]) -> (HashMap<u32, Vec<u32>>, Option<u32>) {
107 let r = crate::relationships::relationships(content);
108 (r.spatial_children, r.project)
109}
110
111pub(crate) fn decode_one(content: &[u8], id: u32) -> Option<DecodedEntity> {
119 let mut decoder = EntityDecoder::new(content);
120 let mut scanner = EntityScanner::new(content);
121 while let Some((entity_id, _type_name, start, end)) = scanner.next_entity() {
122 if entity_id == id {
123 return decoder.decode_at(start, end).ok();
124 }
125 }
126 None
127}
128
129pub(crate) fn project_name(content: &[u8], project_id: u32) -> String {
132 decode_one(content, project_id)
133 .and_then(|e| e.get(2).and_then(|a| a.as_string()).map(|s| s.to_string()))
134 .filter(|s| !s.is_empty())
135 .unwrap_or_else(|| "Project".to_string())
136}
137
138fn build_node_attributes(row: Option<&EntityRow>, ifc_type: &str, opts: &Ifc5Options) -> Map<String, Value> {
139 let mut attrs = Map::new();
140 attrs.insert("bsi::ifc::class".into(), json!({ "code": ifc_type }));
141 if let Some(r) = row {
142 if let Some(n) = &r.name {
143 attrs.insert("bsi::ifc::prop::Name".into(), json!(n));
144 }
145 if let Some(d) = &r.description {
146 attrs.insert("bsi::ifc::prop::Description".into(), json!(d));
147 }
148 for ps in &r.property_sets {
149 for p in &ps.properties {
150 if opts.only_known_properties && !KNOWN_PROPS.contains(&p.name.as_str()) {
151 continue;
152 }
153 attrs.insert(format!("bsi::ifc::prop::{}", p.name), typed_value(p));
154 }
155 }
156 }
157 attrs
158}
159
160pub fn export_ifc5(content: &[u8], opts: &Ifc5Options) -> String {
162 let model = build_export_model(content);
163 let by_id: HashMap<u32, &EntityRow> = model.entities.iter().map(|e| (e.express_id, e)).collect();
164 let (children, project) = spatial_children(content);
165
166 let mut name_of: HashMap<u32, (String, String)> = HashMap::new(); for e in &model.entities {
170 name_of.insert(e.express_id, (e.name.clone().unwrap_or_default(), e.ifc_type.clone()));
171 }
172
173 let mut emit: Vec<u32> = Vec::new();
176 let mut seen: HashSet<u32> = HashSet::new();
177 if let Some(pid) = project {
178 name_of
179 .entry(pid)
180 .or_insert_with(|| (project_name(content, pid), "IfcProject".to_string()));
181 let mut stack = vec![pid];
182 while let Some(id) = stack.pop() {
183 if !seen.insert(id) {
184 continue;
185 }
186 emit.push(id);
187 if let Some(ch) = children.get(&id) {
188 for &c in ch {
189 if !seen.contains(&c) {
190 stack.push(c);
191 }
192 }
193 }
194 }
195 } else {
196 for e in &model.entities {
198 emit.push(e.express_id);
199 }
200 }
201
202 let mut data: Vec<Value> = Vec::with_capacity(emit.len());
203 for id in &emit {
204 let (_name, ifc_type) = name_of
205 .get(id)
206 .cloned()
207 .unwrap_or_else(|| (String::new(), "IfcProduct".to_string()));
208 let mut node = Map::new();
209 node.insert("path".into(), json!(path_for_id(*id, &by_id)));
210
211 if let Some(ch) = children.get(id) {
212 let mut child_map = Map::new();
213 for &c in ch {
214 if !seen.contains(&c) {
215 continue;
216 }
217 let (cname, ctype) = name_of.get(&c).cloned().unwrap_or_default();
218 child_map.insert(prim_name(&cname, &ctype, c), json!(path_for_id(c, &by_id)));
219 }
220 if !child_map.is_empty() {
221 node.insert("children".into(), Value::Object(child_map));
222 }
223 }
224
225 let attrs = build_node_attributes(by_id.get(id).copied(), &ifc_type, opts);
226 if !attrs.is_empty() {
227 node.insert("attributes".into(), Value::Object(attrs));
228 }
229 data.push(Value::Object(node));
230 }
231
232 let doc = json!({
233 "header": {
234 "ifcxVersion": IFCX_VERSION,
240 "author": opts.author,
241 "dataVersion": opts.data_version,
242 },
243 "imports": [ { "uri": IMPORT_CORE }, { "uri": IMPORT_PROP } ],
244 "schemas": {},
245 "data": data,
246 });
247
248 if opts.pretty {
249 serde_json::to_string_pretty(&doc).expect("ifcx serializes")
250 } else {
251 serde_json::to_string(&doc).expect("ifcx serializes")
252 }
253}
254
255#[cfg(test)]
256#[path = "ifc5_tests.rs"]
257mod tests;