1use crate::error::{Error, Result};
11use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
12use nalgebra::{Matrix4, Point3, Vector3};
13
14mod placement_2d;
15pub(crate) use placement_2d::parse_axis2_placement_2d;
16
17pub fn parse_axis2_placement_3d(
27 placement: &DecodedEntity,
28 decoder: &mut EntityDecoder,
29) -> Result<Matrix4<f64>> {
30 let location = parse_cartesian_point(placement, decoder, 0)?;
32
33 let z_axis = if let Some(axis_attr) = placement.get(1) {
35 if !axis_attr.is_null() {
36 if let Some(axis_entity) = decoder.resolve_ref(axis_attr)? {
37 parse_direction(&axis_entity)?
38 } else {
39 Vector3::new(0.0, 0.0, 1.0)
40 }
41 } else {
42 Vector3::new(0.0, 0.0, 1.0)
43 }
44 } else {
45 Vector3::new(0.0, 0.0, 1.0)
46 };
47
48 let x_axis = if let Some(ref_dir_attr) = placement.get(2) {
50 if !ref_dir_attr.is_null() {
51 if let Some(ref_dir_entity) = decoder.resolve_ref(ref_dir_attr)? {
52 parse_direction(&ref_dir_entity)?
53 } else {
54 Vector3::new(1.0, 0.0, 0.0)
55 }
56 } else {
57 Vector3::new(1.0, 0.0, 0.0)
58 }
59 } else {
60 Vector3::new(1.0, 0.0, 0.0)
61 };
62
63 Ok(build_axis2_matrix(location, z_axis, x_axis))
64}
65
66pub(crate) fn build_axis2_matrix(
79 location: Point3<f64>,
80 z_axis: Vector3<f64>,
81 x_axis: Vector3<f64>,
82) -> Matrix4<f64> {
83 let z_axis_final = z_axis
89 .try_normalize(1e-9)
90 .unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
91 let x_axis_normalized = x_axis
92 .try_normalize(1e-9)
93 .unwrap_or_else(|| Vector3::new(1.0, 0.0, 0.0));
94
95 let dot_product = x_axis_normalized.dot(&z_axis_final);
97 let x_axis_orthogonal = x_axis_normalized - z_axis_final * dot_product;
98 let x_axis_final = if x_axis_orthogonal.norm() > 1e-6 {
99 x_axis_orthogonal.normalize()
100 } else {
101 if z_axis_final.z.abs() < 0.9 {
103 Vector3::new(0.0, 0.0, 1.0).cross(&z_axis_final).normalize()
104 } else {
105 Vector3::new(1.0, 0.0, 0.0).cross(&z_axis_final).normalize()
106 }
107 };
108
109 let y_axis = z_axis_final.cross(&x_axis_final).normalize();
111
112 let mut transform = Matrix4::identity();
115 transform[(0, 0)] = x_axis_final.x;
116 transform[(1, 0)] = x_axis_final.y;
117 transform[(2, 0)] = x_axis_final.z;
118 transform[(0, 1)] = y_axis.x;
119 transform[(1, 1)] = y_axis.y;
120 transform[(2, 1)] = y_axis.z;
121 transform[(0, 2)] = z_axis_final.x;
122 transform[(1, 2)] = z_axis_final.y;
123 transform[(2, 2)] = z_axis_final.z;
124 transform[(0, 3)] = location.x;
125 transform[(1, 3)] = location.y;
126 transform[(2, 3)] = location.z;
127
128 transform
129}
130
131pub fn parse_cartesian_point(
135 parent: &DecodedEntity,
136 decoder: &mut EntityDecoder,
137 attr_index: usize,
138) -> Result<Point3<f64>> {
139 let point_attr = parent
140 .get(attr_index)
141 .ok_or_else(|| Error::geometry("Missing cartesian point".to_string()))?;
142
143 if let Some(point_id) = point_attr.as_entity_ref() {
145 if let Some((x, y, z)) = decoder.get_cartesian_point_fast(point_id) {
146 return Ok(Point3::new(x, y, z));
147 }
148 }
149
150 let point_entity = decoder
152 .resolve_ref(point_attr)?
153 .ok_or_else(|| Error::geometry("Failed to resolve cartesian point".to_string()))?;
154
155 parse_decoded_cartesian_point(&point_entity)
156}
157
158pub(crate) fn parse_decoded_cartesian_point(point_entity: &DecodedEntity) -> Result<Point3<f64>> {
160 if point_entity.ifc_type != IfcType::IfcCartesianPoint {
161 return Err(Error::geometry(format!(
162 "Expected IfcCartesianPoint, got {}",
163 point_entity.ifc_type
164 )));
165 }
166
167 let coords_attr = point_entity
169 .get(0)
170 .ok_or_else(|| Error::geometry("IfcCartesianPoint missing coordinates".to_string()))?;
171
172 let coords = coords_attr
173 .as_list()
174 .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
175
176 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
177 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
178 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
179
180 Ok(Point3::new(x, y, z))
181}
182
183pub fn parse_cartesian_point_from_id(
187 point_id: u32,
188 decoder: &mut EntityDecoder,
189) -> Result<Point3<f64>> {
190 if let Some((x, y, z)) = decoder.get_cartesian_point_fast(point_id) {
192 return Ok(Point3::new(x, y, z));
193 }
194
195 let point_entity = decoder.decode_by_id(point_id)?;
197
198 if point_entity.ifc_type != IfcType::IfcCartesianPoint {
199 return Err(Error::geometry(format!(
200 "Expected IfcCartesianPoint, got {}",
201 point_entity.ifc_type
202 )));
203 }
204
205 let coords_attr = point_entity
207 .get(0)
208 .ok_or_else(|| Error::geometry("IfcCartesianPoint missing coordinates".to_string()))?;
209
210 let coords = coords_attr
211 .as_list()
212 .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
213
214 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
215 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
216 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
217
218 Ok(Point3::new(x, y, z))
219}
220
221pub fn parse_direction_from_id(dir_id: u32, decoder: &mut EntityDecoder) -> Result<Vector3<f64>> {
226 let dir = decoder.decode_by_id(dir_id)?;
227 parse_direction(&dir)
228}
229
230pub fn parse_axis2_placement_3d_from_id(
234 placement_id: u32,
235 decoder: &mut EntityDecoder,
236) -> Result<Matrix4<f64>> {
237 let placement = decoder.decode_by_id(placement_id)?;
238
239 let location = if let Some(loc_attr) = placement.get(0) {
241 if let Some(loc_id) = loc_attr.as_entity_ref() {
242 parse_cartesian_point_from_id(loc_id, decoder)?
243 } else {
244 Point3::new(0.0, 0.0, 0.0)
245 }
246 } else {
247 Point3::new(0.0, 0.0, 0.0)
248 };
249
250 let z_axis = if let Some(axis_attr) = placement.get(1) {
252 if !axis_attr.is_null() {
253 if let Some(axis_id) = axis_attr.as_entity_ref() {
254 parse_direction_from_id(axis_id, decoder)?
255 } else {
256 Vector3::new(0.0, 0.0, 1.0)
257 }
258 } else {
259 Vector3::new(0.0, 0.0, 1.0)
260 }
261 } else {
262 Vector3::new(0.0, 0.0, 1.0)
263 };
264
265 let x_axis = if let Some(ref_dir_attr) = placement.get(2) {
267 if !ref_dir_attr.is_null() {
268 if let Some(ref_dir_id) = ref_dir_attr.as_entity_ref() {
269 parse_direction_from_id(ref_dir_id, decoder)?
270 } else {
271 Vector3::new(1.0, 0.0, 0.0)
272 }
273 } else {
274 Vector3::new(1.0, 0.0, 0.0)
275 }
276 } else {
277 Vector3::new(1.0, 0.0, 0.0)
278 };
279
280 Ok(build_axis2_matrix(location, z_axis, x_axis))
281}
282
283pub fn parse_direction(direction_entity: &DecodedEntity) -> Result<Vector3<f64>> {
287 if direction_entity.ifc_type != IfcType::IfcDirection {
288 return Err(Error::geometry(format!(
289 "Expected IfcDirection, got {}",
290 direction_entity.ifc_type
291 )));
292 }
293
294 let ratios_attr = direction_entity
296 .get(0)
297 .ok_or_else(|| Error::geometry("IfcDirection missing ratios".to_string()))?;
298
299 let ratios = ratios_attr
300 .as_list()
301 .ok_or_else(|| Error::geometry("Expected ratio list".to_string()))?;
302
303 let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
304 let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
305 let z = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
306
307 Ok(Vector3::new(x, y, z))
308}
309
310pub fn rotation_angle_about_z(matrix: &[f64; 16]) -> Option<f64> {
325 let x = matrix[0];
326 let y = matrix[1];
327 if x * x + y * y < 1e-10 {
328 return None;
329 }
330 Some(y.atan2(x))
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn test_parse_direction() {
339 let content = "#1=IFCDIRECTION((0.5,0.25,0.75));";
341 let mut decoder = EntityDecoder::new(content);
342 let dir = decoder.decode_by_id(1).unwrap();
343
344 let v = parse_direction(&dir).unwrap();
345
346 assert!((v.x - 0.5).abs() < 1e-12);
347 assert!((v.y - 0.25).abs() < 1e-12);
348 assert!((v.z - 0.75).abs() < 1e-12);
349 }
350
351 #[test]
352 fn parse_cartesian_point_reads_coordinates_from_attribute_0() {
353 let content = "\
356#1=IFCCARTESIANPOINT((1.0,2.0,3.0));
357#2=IFCAXIS2PLACEMENT3D(#1,$,$);";
358 let mut decoder = EntityDecoder::new(content);
359 let placement = decoder.decode_by_id(2).unwrap();
360
361 let p = parse_cartesian_point(&placement, &mut decoder, 0).unwrap();
362
363 assert!((p.x - 1.0).abs() < 1e-12);
364 assert!((p.y - 2.0).abs() < 1e-12);
365 assert!((p.z - 3.0).abs() < 1e-12);
366 }
367
368 #[test]
369 fn parse_axis2_placement_3d_defaults_missing_axis_and_ref_direction() {
370 let content = "\
375#1=IFCCARTESIANPOINT((10.0,20.0,30.0));
376#2=IFCAXIS2PLACEMENT3D(#1);";
377 let mut decoder = EntityDecoder::new(content);
378 let placement = decoder.decode_by_id(2).unwrap();
379 assert_eq!(placement.attributes.len(), 1, "test fixture sanity check");
380
381 let m = parse_axis2_placement_3d(&placement, &mut decoder).unwrap();
382
383 assert!((m[(0, 3)] - 10.0).abs() < 1e-9);
385 assert!((m[(1, 3)] - 20.0).abs() < 1e-9);
386 assert!((m[(2, 3)] - 30.0).abs() < 1e-9);
387 assert!((m[(0, 2)] - 0.0).abs() < 1e-9);
389 assert!((m[(1, 2)] - 0.0).abs() < 1e-9);
390 assert!((m[(2, 2)] - 1.0).abs() < 1e-9);
391 assert!((m[(0, 0)] - 1.0).abs() < 1e-9);
393 assert!((m[(1, 0)] - 0.0).abs() < 1e-9);
394 assert!((m[(2, 0)] - 0.0).abs() < 1e-9);
395 }
396
397 #[test]
402 fn parse_axis2_placement_3d_axis_only_frames_on_world_axes() {
403 type V = [f64; 3];
404 let cases: [(V, V, V); 6] = [
405 ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]),
407 ([-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0]),
408 ([0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0]),
409 ([0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]),
410 ([0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
411 ([0.0, 0.0, -1.0], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0]),
412 ];
413 for ((axis, x, y), tail) in cases.iter().flat_map(|c| [(c, ",$"), (c, "")]) {
414 let content = format!(
415 "#1=IFCCARTESIANPOINT((0.0,0.0,0.0));\n#2=IFCDIRECTION(({:?},{:?},{:?}));\n#3=IFCAXIS2PLACEMENT3D(#1,#2{tail});",
416 axis[0], axis[1], axis[2]
417 );
418 let mut decoder = EntityDecoder::new(&content);
419 let placement = decoder.decode_by_id(3).unwrap();
420 let m = parse_axis2_placement_3d(&placement, &mut decoder).unwrap();
421 for i in 0..3 {
422 assert!((m[(i, 0)] - x[i]).abs() < 1e-9, "Axis {axis:?}{tail}: X = {:?}", m.column(0));
423 assert!((m[(i, 1)] - y[i]).abs() < 1e-9, "Axis {axis:?}{tail}: Y = {:?}", m.column(1));
424 }
425 }
426 }
427
428 #[test]
429 fn parse_axis2_placement_3d_orthogonalizes_parallel_ref_direction_low_z() {
430 let content = "\
434#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
435#2=IFCDIRECTION((1.0,0.0,0.0));
436#3=IFCDIRECTION((1.0,0.0,0.0));
437#4=IFCAXIS2PLACEMENT3D(#1,#2,#3);";
438 let mut decoder = EntityDecoder::new(content);
439 let placement = decoder.decode_by_id(4).unwrap();
440
441 let m = parse_axis2_placement_3d(&placement, &mut decoder).unwrap();
442
443 assert!((m[(0, 0)] - 0.0).abs() < 1e-9);
446 assert!((m[(1, 0)] - 1.0).abs() < 1e-9);
447 assert!((m[(2, 0)] - 0.0).abs() < 1e-9);
448 assert!((m[(0, 1)] - 0.0).abs() < 1e-9);
450 assert!((m[(1, 1)] - 0.0).abs() < 1e-9);
451 assert!((m[(2, 1)] - 1.0).abs() < 1e-9);
452 }
453
454 #[test]
455 fn parse_axis2_placement_3d_orthogonalizes_parallel_ref_direction_high_z() {
456 let content = "\
460#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
461#2=IFCDIRECTION((0.0,0.0,1.0));
462#3=IFCDIRECTION((0.0,0.0,1.0));
463#4=IFCAXIS2PLACEMENT3D(#1,#2,#3);";
464 let mut decoder = EntityDecoder::new(content);
465 let placement = decoder.decode_by_id(4).unwrap();
466
467 let m = parse_axis2_placement_3d(&placement, &mut decoder).unwrap();
468
469 assert!((m[(0, 0)] - 0.0).abs() < 1e-9);
471 assert!((m[(1, 0)] - (-1.0)).abs() < 1e-9);
472 assert!((m[(2, 0)] - 0.0).abs() < 1e-9);
473 assert!((m[(0, 1)] - 1.0).abs() < 1e-9);
475 assert!((m[(1, 1)] - 0.0).abs() < 1e-9);
476 assert!((m[(2, 1)] - 0.0).abs() < 1e-9);
477 }
478
479 #[test]
480 fn rotation_angle_about_z_handles_identity_rotation_and_scale() {
481 let mut m = [0.0f64; 16];
483 m[0] = 1.0;
484 m[5] = 1.0;
485 m[10] = 1.0;
486 m[15] = 1.0;
487 assert!(rotation_angle_about_z(&m).unwrap().abs() < 1e-12);
488
489 let a = std::f64::consts::FRAC_PI_4;
492 let mut r = [0.0f64; 16];
493 r[0] = a.cos();
494 r[1] = a.sin();
495 r[4] = -a.sin();
496 r[5] = a.cos();
497 r[10] = 1.0;
498 r[15] = 1.0;
499 assert!((rotation_angle_about_z(&r).unwrap() - a).abs() < 1e-12);
500
501 let mut s = r;
503 for v in s.iter_mut().take(3) {
504 *v *= 3.0;
505 }
506 assert!((rotation_angle_about_z(&s).unwrap() - a).abs() < 1e-9);
507
508 assert!(rotation_angle_about_z(&[0.0f64; 16]).is_none());
510 }
511
512 #[test]
515 fn build_axis2_matrix_zero_axis_stays_finite() {
516 let m = build_axis2_matrix(
517 Point3::new(0.0, 0.0, 0.0),
518 Vector3::new(0.0, 0.0, 0.0),
519 Vector3::new(1.0, 0.0, 0.0),
520 );
521 assert!(m.iter().all(|v| v.is_finite()), "NaN in matrix: {m:?}");
522 }
523}