ifc_lite_export/
step_map_transform.rs1use std::collections::{BTreeMap, HashMap};
6use ifc_lite_core::{EntityDecoder, EntityScanner, GeoRefExtractor, IfcType};
7use ifc_lite_geometry::GeometryRouter;
8use nalgebra::Matrix4;
9use serde::Serialize;
10
11#[path = "step_map_transform_preflight.rs"]
12pub(crate) mod preflight;
13#[path = "step_map_transform_writer.rs"]
14mod writer;
15#[path = "step_map_rigid.rs"]
16mod rigid;
17#[path = "step_map_rigid_ownership.rs"]
18mod rigid_ownership;
19#[path = "step_map_rigid_contexts.rs"]
20mod rigid_contexts;
21#[path = "step_map_context_metadata.rs"]
22mod context_metadata;
23
24#[derive(Debug, Serialize)]
27#[serde(rename_all = "camelCase")]
28pub struct MapConversionEntityPatch {
29 pub express_id: u32,
30 pub line: String,
31}
32
33#[derive(Debug, Default, Serialize)]
36#[serde(rename_all = "camelCase")]
37pub struct MapConversionNormalizationPlan {
38 pub replacements: Vec<MapConversionEntityPatch>,
39 pub new_entities: Vec<MapConversionEntityPatch>,
40 pub warnings: Vec<String>,
41}
42
43pub(super) struct Record<'a> { pub id: u32, pub kind: IfcType, pub line: &'a str }
44
45pub fn plan_map_conversion_normalization(content: &[u8]) -> Result<MapConversionNormalizationPlan, String> {
52 let text = std::str::from_utf8(content).map_err(|_| "map normalization requires UTF-8 STEP".to_string())?;
53 let mut scanner = EntityScanner::new(content);
54 let mut records = Vec::new();
55 let mut ids = HashMap::new();
56 while let Some((id, name, start, end)) = scanner.next_entity() {
57 if ids.insert(id, records.len()).is_some() {
58 return Err(format!("map normalization refuses duplicate entity #{id}"));
59 }
60 let line = text.get(start..end).ok_or("invalid STEP record boundary")?;
61 records.push(Record { id, kind: IfcType::from_str(name), line });
62 }
63 let maps: Vec<_> = records.iter().filter(|record| matches!(record.kind,
64 IfcType::IfcMapConversion | IfcType::IfcMapConversionScaled)).collect();
65 if maps.is_empty() { return Ok(MapConversionNormalizationPlan::default()); }
66 let attempt = || -> Result<MapConversionNormalizationPlan, String> {
67 let schema = crate::source_header::declared_schema(content)
68 .ok_or("map normalization requires a declared source schema")?.to_ascii_uppercase();
69 if schema != "IFC4" && !schema.starts_with("IFC4_ADD")
70 && schema != "IFC4X3" && !schema.starts_with("IFC4X3_ADD") {
71 return Err(format!("source schema {schema} is not supported by map normalization"));
72 }
73 if maps.len() != 1 { return Err("multiple map conversions are not supported atomically".into()); }
74 let map = maps[0];
75 let mut decoder = EntityDecoder::new(&content);
76 let operation = decoder.decode_by_id(map.id).map_err(|error| error.to_string())?;
77 preflight::operation(&operation)?;
78 let target_id = operation.get_ref(1).ok_or("map TargetCRS is missing")?;
79 let target = decoder.decode_by_id(target_id).map_err(|error| error.to_string())?;
80 if target.ifc_type != IfcType::IfcProjectedCRS { return Err("map TargetCRS is not an IfcProjectedCRS".into()); }
81 let types = [(map.id, map.kind.clone()), (target_id, IfcType::IfcProjectedCRS)];
84 let projects: Vec<_> = records.iter().filter(|record| record.kind == IfcType::IfcProject).collect();
85 if projects.len() != 1 { return Err("exactly one IfcProject is required".into()); }
86 preflight::project_units(projects[0].id, &mut decoder)?;
87 let unit = ifc_lite_core::try_extract_length_unit_scale(&mut decoder, projects[0].id)
88 .ok_or("project length unit cannot be resolved")?;
89 if !unit.is_finite() || unit <= 0.0 { return Err("project length unit is not positive and finite".into()); }
90 preflight::map_unit(&target, unit, &mut decoder)?;
94 let geo = GeoRefExtractor::extract(&mut decoder, &types)
95 .map_err(|error| error.to_string())?.filter(|geo| geo.has_map_conversion)
96 .ok_or_else(|| format!("map conversion #{} cannot be resolved exactly", map.id))?;
97 if geo.factor_x != 1.0 || geo.factor_y != 1.0 || geo.factor_z != 1.0 {
98 return Err("per-axis map factors are not supported by uniform mapped normalization".into());
99 }
100 let scale = geo.scale / unit;
101 let mut affine = Matrix4::from_column_slice(&geo.to_matrix());
102 for row in 0..3 { for col in 0..4 { affine[(row, col)] /= unit; } }
103 if !scale.is_finite() || scale <= 0.0 || !affine.iter().all(|value| value.is_finite()) {
104 return Err("map similarity is not positive and finite".into());
105 }
106 if scale == 1.0 && geo.x_axis_abscissa == 1.0 && geo.x_axis_ordinate.abs() <= f64::EPSILON {
111 return Ok(MapConversionNormalizationPlan::default());
112 }
113 let context = operation.get_ref(0).ok_or("map SourceCRS is missing")?;
114 if scale == 1.0 {
115 return rigid::plan(&records, &ids, map, context, unit, &affine, &mut decoder);
116 }
117 let type_maps = preflight::model(&records, context, &mut decoder)?;
118 let router = GeometryRouter::with_scale(unit);
119 let mut plan = MapConversionNormalizationPlan::default();
120 let mut output = writer::Writer::new(&records)?;
121 let inverse_scale = Matrix4::new_nonuniform_scaling(&nalgebra::Vector3::repeat(1.0 / scale));
122 let mut shapes = BTreeMap::new();
123 let mut validated = std::collections::HashSet::new();
124 for record in records.iter().filter(|record| record.kind.is_subtype_of(IfcType::IfcProduct)) {
125 let product = decoder.decode_by_id(record.id).map_err(|error| error.to_string())?;
126 preflight::placement(&product, &router, &mut validated, &mut decoder)?;
127 let matrix = router.resolve_scaled_placement_strict(&product, &mut decoder)
128 .map_err(|error| error.to_string())?;
129 let mut placement = Matrix4::from_column_slice(&matrix);
130 for row in 0..3 { placement[(row, 3)] /= unit; }
131 let normalized = affine * placement * inverse_scale;
132 preflight::rigid_frame(&normalized, record.id)?;
133 let new_placement = output.placement(&normalized)?;
134 plan.replacements.push(writer::replace(record, &[(5, format!("#{new_placement}"))])?);
135 if let Some(shape) = product.get_ref(6) { shapes.insert(shape, ()); }
136 }
137 for shape_id in shapes.keys() {
138 let shape = decoder.decode_by_id(*shape_id).map_err(|error| error.to_string())?;
139 let reps = shape.get_refs(2).ok_or("ProductDefinitionShape has no Representations")?;
140 let mut mapped = Vec::new();
141 for rep_id in reps {
142 let record = &records[*ids.get(&rep_id).ok_or("representation record is absent")?];
143 mapped.push(output.mapped_representation(record, context, scale)?);
144 }
145 let record = &records[*ids.get(shape_id).ok_or("ProductDefinitionShape record is absent")?];
146 plan.replacements.push(writer::replace(record, &[(2, writer::refs(&mapped))])?);
147 }
148 for map_id in type_maps {
153 let record = &records[*ids.get(&map_id).ok_or("type map record is absent")?];
154 let origin = crate::step_text::attribute_of(record.line, 0).ok_or("type map MappingOrigin is invalid")?.trim().to_string();
155 let rep_id = decoder.decode_by_id(map_id).map_err(|error| error.to_string())?.get_ref(1).ok_or("type map has no MappedRepresentation")?;
156 let rep = &records[*ids.get(&rep_id).ok_or("type map representation record is absent")?];
157 let wrapped = output.scaled_wrapper(rep, &origin, scale)?;
158 let identity = output.identity_frame()?;
159 plan.replacements.push(writer::replace(record, &[(0, format!("#{identity}")), (1, format!("#{wrapped}"))])?);
160 }
161 let mut edits = vec![(2, "0.".into()), (3, "0.".into()), (4, "0.".into()),
162 (5, "1.".into()), (6, "0.".into()), (7, writer::real(unit)?)];
163 if map.kind == IfcType::IfcMapConversionScaled {
164 edits.extend([(8, "1.".into()), (9, "1.".into()), (10, "1.".into())]);
165 }
166 plan.replacements.push(writer::replace(map, &edits)?);
167 context_metadata::transform_north(&records, &ids, &std::collections::HashSet::from([context]), &affine, &mut decoder, &mut output, &mut plan)?;
168 plan.new_entities = output.finish();
169 Ok(plan)
170 };
171 Ok(attempt().unwrap_or_else(|error| MapConversionNormalizationPlan {
172 warnings: vec![format!("Map geometry normalization refused: {error}. No context geometry was changed.")],
173 ..Default::default()
174 }))
175}
176
177#[cfg(test)]
178#[path = "step_map_transform_tests.rs"]
179mod tests;
180
181#[cfg(test)]
182#[path = "step_map_rigid_tests.rs"]
183mod rigid_tests;
184
185#[cfg(test)]
186#[path = "step_map_rigid_fixture_tests.rs"]
187mod rigid_fixture_tests;