ifc_lite_processing/appearance/
catalog.rs1use super::source::Source;
6use ifc_lite_core::{AttributeValue as A, IfcType};
7use serde::{Deserialize, Serialize};
8use std::collections::{BTreeMap, BTreeSet};
9
10#[derive(Debug, Clone, Deserialize, Serialize)]
11#[serde(rename_all = "camelCase", deny_unknown_fields)]
12pub struct AppearanceCatalogRequest {
13 pub schema: String,
14 pub source_revision: String,
15 pub product_ids: Vec<u32>,
16}
17#[derive(Debug, Clone, Serialize)]
18#[serde(rename_all = "camelCase")]
19pub struct AppearanceCatalogProduct {
20 pub product_id: u32,
21 pub ifc_class: String,
22 pub type_ids: Vec<u32>,
23}
24#[derive(Debug, Clone, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub struct AppearanceCatalogType {
27 pub type_id: u32,
28 pub ifc_class: String,
29 #[serde(rename = "Name")]
31 pub name: Option<String>,
32}
33#[derive(Debug, Clone, Serialize)]
34#[serde(rename_all = "camelCase")]
35pub struct AppearanceCatalog {
36 pub source_revision: String,
37 pub products: Vec<AppearanceCatalogProduct>,
38 pub types: Vec<AppearanceCatalogType>,
39 pub missing_product_ids: Vec<u32>,
41}
42
43pub fn catalog_appearance(bytes: &[u8], request: &AppearanceCatalogRequest) -> Result<AppearanceCatalog, String> {
47 if request.schema != "IFC4" && request.schema != "IFC4X3" {
48 return Err("Appearance catalog requires IFC4 or IFC4X3".into());
49 }
50 if request.product_ids.len() > 10_000 || request.product_ids.contains(&0) || request.source_revision.len() > 4096 {
51 return Err("Appearance catalog request exceeds its owner/revision budget".into());
52 }
53 let mut source = Source::new(bytes)?;
56 let mut products = BTreeMap::<u32, (IfcType, BTreeSet<u32>)>::new();
57 let mut missing = BTreeSet::new();
58 for &id in &request.product_ids {
59 match source.types.get(&id).copied() {
60 Some(class) if class.is_subtype_of(IfcType::IfcProduct) => { products.entry(id).or_insert((class, BTreeSet::new())); }
61 _ => { missing.insert(id); }
62 }
63 }
64 let relation_ids: Vec<u32> = source.types.iter().filter_map(|(&id, class)|
65 (*class == IfcType::IfcRelDefinesByType).then_some(id)).collect();
66 let mut types = BTreeMap::new();
67 let mut memberships = 0usize;
68 let mut label_bytes = 0usize;
69 for id in relation_ids {
70 let relation = source.entity(id)?;
71 let related = relation.get(4).and_then(A::as_list)
72 .ok_or_else(|| format!("Invalid IfcRelDefinesByType #{id} RelatedObjects"))?;
73 let mut owners = BTreeSet::new();
74 for member in related {
75 let owner = member.as_entity_ref().ok_or_else(|| format!("Invalid IfcRelDefinesByType #{id} member"))?;
76 if products.contains_key(&owner) { owners.insert(owner); }
77 }
78 if owners.is_empty() { continue; }
79 let type_id = relation.get_ref(5).ok_or_else(|| format!("Missing IfcRelDefinesByType #{id} RelatingType"))?;
80 let class = source.types.get(&type_id).copied().filter(|class| class.is_subtype_of(IfcType::IfcTypeObject))
81 .ok_or_else(|| format!("Invalid IfcRelDefinesByType #{id} RelatingType #{type_id}"))?;
82 if let std::collections::btree_map::Entry::Vacant(entry) = types.entry(type_id) {
83 let entity = source.entity(type_id)?;
84 let name = match entity.get(2) {
85 None | Some(A::Null) => None,
86 Some(value) => Some(value.as_string().ok_or("Invalid IFC type Name")?),
87 };
88 label_bytes += name.map_or(0, str::len);
89 if label_bytes > 4 * 1024 * 1024 { return Err("Appearance catalog type names exceed 4 MiB metadata budget".into()); }
90 entry.insert(AppearanceCatalogType { type_id, ifc_class: class.name().into(), name: name.map(str::to_owned) });
91 }
92 for owner in owners {
93 if products.get_mut(&owner).expect("owner was selected above").1.insert(type_id) {
94 memberships += 1;
95 if memberships > 200_000 { return Err("Appearance catalog exceeds 200000 type memberships".into()); }
96 }
97 }
98 }
99 Ok(AppearanceCatalog {
100 source_revision: request.source_revision.clone(),
101 products: products.into_iter().map(|(product_id, (class, ids))| AppearanceCatalogProduct {
102 product_id, ifc_class: class.name().into(), type_ids: ids.into_iter().collect(),
103 }).collect(),
104 types: types.into_values().collect(), missing_product_ids: missing.into_iter().collect(),
105 })
106}
107
108#[cfg(test)]
109#[path = "catalog_tests.rs"]
110mod tests;