1use crate::error::{Error, Result};
11use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
12use nalgebra::{Matrix4, Point3, Vector3};
13
14pub fn parse_axis2_placement_3d(
24 placement: &DecodedEntity,
25 decoder: &mut EntityDecoder,
26) -> Result<Matrix4<f64>> {
27 let location = parse_cartesian_point(placement, decoder, 0)?;
29
30 let z_axis = if let Some(axis_attr) = placement.get(1) {
32 if !axis_attr.is_null() {
33 if let Some(axis_entity) = decoder.resolve_ref(axis_attr)? {
34 parse_direction(&axis_entity)?
35 } else {
36 Vector3::new(0.0, 0.0, 1.0)
37 }
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
45 let x_axis = if let Some(ref_dir_attr) = placement.get(2) {
47 if !ref_dir_attr.is_null() {
48 if let Some(ref_dir_entity) = decoder.resolve_ref(ref_dir_attr)? {
49 parse_direction(&ref_dir_entity)?
50 } else {
51 Vector3::new(1.0, 0.0, 0.0)
52 }
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
60 Ok(build_axis2_matrix(location, z_axis, x_axis))
61}
62
63pub(crate) fn build_axis2_matrix(
76 location: Point3<f64>,
77 z_axis: Vector3<f64>,
78 x_axis: Vector3<f64>,
79) -> Matrix4<f64> {
80 let z_axis_final = z_axis
86 .try_normalize(1e-9)
87 .unwrap_or_else(|| Vector3::new(0.0, 0.0, 1.0));
88 let x_axis_normalized = x_axis
89 .try_normalize(1e-9)
90 .unwrap_or_else(|| Vector3::new(1.0, 0.0, 0.0));
91
92 let dot_product = x_axis_normalized.dot(&z_axis_final);
94 let x_axis_orthogonal = x_axis_normalized - z_axis_final * dot_product;
95 let x_axis_final = if x_axis_orthogonal.norm() > 1e-6 {
96 x_axis_orthogonal.normalize()
97 } else {
98 if z_axis_final.z.abs() < 0.9 {
100 Vector3::new(0.0, 0.0, 1.0).cross(&z_axis_final).normalize()
101 } else {
102 Vector3::new(1.0, 0.0, 0.0).cross(&z_axis_final).normalize()
103 }
104 };
105
106 let y_axis = z_axis_final.cross(&x_axis_final).normalize();
108
109 let mut transform = Matrix4::identity();
112 transform[(0, 0)] = x_axis_final.x;
113 transform[(1, 0)] = x_axis_final.y;
114 transform[(2, 0)] = x_axis_final.z;
115 transform[(0, 1)] = y_axis.x;
116 transform[(1, 1)] = y_axis.y;
117 transform[(2, 1)] = y_axis.z;
118 transform[(0, 2)] = z_axis_final.x;
119 transform[(1, 2)] = z_axis_final.y;
120 transform[(2, 2)] = z_axis_final.z;
121 transform[(0, 3)] = location.x;
122 transform[(1, 3)] = location.y;
123 transform[(2, 3)] = location.z;
124
125 transform
126}
127
128pub fn parse_cartesian_point(
132 parent: &DecodedEntity,
133 decoder: &mut EntityDecoder,
134 attr_index: usize,
135) -> Result<Point3<f64>> {
136 let point_attr = parent
137 .get(attr_index)
138 .ok_or_else(|| Error::geometry("Missing cartesian point".to_string()))?;
139
140 if let Some(point_id) = point_attr.as_entity_ref() {
142 if let Some((x, y, z)) = decoder.get_cartesian_point_fast(point_id) {
143 return Ok(Point3::new(x, y, z));
144 }
145 }
146
147 let point_entity = decoder
149 .resolve_ref(point_attr)?
150 .ok_or_else(|| Error::geometry("Failed to resolve cartesian point".to_string()))?;
151
152 if point_entity.ifc_type != IfcType::IfcCartesianPoint {
153 return Err(Error::geometry(format!(
154 "Expected IfcCartesianPoint, got {}",
155 point_entity.ifc_type
156 )));
157 }
158
159 let coords_attr = point_entity
161 .get(0)
162 .ok_or_else(|| Error::geometry("IfcCartesianPoint missing coordinates".to_string()))?;
163
164 let coords = coords_attr
165 .as_list()
166 .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
167
168 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
169 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
170 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
171
172 Ok(Point3::new(x, y, z))
173}
174
175pub fn parse_cartesian_point_from_id(
179 point_id: u32,
180 decoder: &mut EntityDecoder,
181) -> Result<Point3<f64>> {
182 if let Some((x, y, z)) = decoder.get_cartesian_point_fast(point_id) {
184 return Ok(Point3::new(x, y, z));
185 }
186
187 let point_entity = decoder.decode_by_id(point_id)?;
189
190 if point_entity.ifc_type != IfcType::IfcCartesianPoint {
191 return Err(Error::geometry(format!(
192 "Expected IfcCartesianPoint, got {}",
193 point_entity.ifc_type
194 )));
195 }
196
197 let coords_attr = point_entity
199 .get(0)
200 .ok_or_else(|| Error::geometry("IfcCartesianPoint missing coordinates".to_string()))?;
201
202 let coords = coords_attr
203 .as_list()
204 .ok_or_else(|| Error::geometry("Expected coordinate list".to_string()))?;
205
206 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
207 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
208 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
209
210 Ok(Point3::new(x, y, z))
211}
212
213pub fn parse_direction_from_id(dir_id: u32, decoder: &mut EntityDecoder) -> Result<Vector3<f64>> {
218 let dir = decoder.decode_by_id(dir_id)?;
219 parse_direction(&dir)
220}
221
222pub fn parse_axis2_placement_3d_from_id(
226 placement_id: u32,
227 decoder: &mut EntityDecoder,
228) -> Result<Matrix4<f64>> {
229 let placement = decoder.decode_by_id(placement_id)?;
230
231 let location = if let Some(loc_attr) = placement.get(0) {
233 if let Some(loc_id) = loc_attr.as_entity_ref() {
234 parse_cartesian_point_from_id(loc_id, decoder)?
235 } else {
236 Point3::new(0.0, 0.0, 0.0)
237 }
238 } else {
239 Point3::new(0.0, 0.0, 0.0)
240 };
241
242 let z_axis = if let Some(axis_attr) = placement.get(1) {
244 if !axis_attr.is_null() {
245 if let Some(axis_id) = axis_attr.as_entity_ref() {
246 parse_direction_from_id(axis_id, decoder)?
247 } else {
248 Vector3::new(0.0, 0.0, 1.0)
249 }
250 } else {
251 Vector3::new(0.0, 0.0, 1.0)
252 }
253 } else {
254 Vector3::new(0.0, 0.0, 1.0)
255 };
256
257 let x_axis = if let Some(ref_dir_attr) = placement.get(2) {
259 if !ref_dir_attr.is_null() {
260 if let Some(ref_dir_id) = ref_dir_attr.as_entity_ref() {
261 parse_direction_from_id(ref_dir_id, decoder)?
262 } else {
263 Vector3::new(1.0, 0.0, 0.0)
264 }
265 } else {
266 Vector3::new(1.0, 0.0, 0.0)
267 }
268 } else {
269 Vector3::new(1.0, 0.0, 0.0)
270 };
271
272 Ok(build_axis2_matrix(location, z_axis, x_axis))
273}
274
275pub fn parse_direction(direction_entity: &DecodedEntity) -> Result<Vector3<f64>> {
279 if direction_entity.ifc_type != IfcType::IfcDirection {
280 return Err(Error::geometry(format!(
281 "Expected IfcDirection, got {}",
282 direction_entity.ifc_type
283 )));
284 }
285
286 let ratios_attr = direction_entity
288 .get(0)
289 .ok_or_else(|| Error::geometry("IfcDirection missing ratios".to_string()))?;
290
291 let ratios = ratios_attr
292 .as_list()
293 .ok_or_else(|| Error::geometry("Expected ratio list".to_string()))?;
294
295 let x = ratios.first().and_then(|v| v.as_float()).unwrap_or(0.0);
296 let y = ratios.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
297 let z = ratios.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
298
299 Ok(Vector3::new(x, y, z))
300}
301
302pub fn rotation_angle_about_z(matrix: &[f64; 16]) -> Option<f64> {
317 let x = matrix[0];
318 let y = matrix[1];
319 if x * x + y * y < 1e-10 {
320 return None;
321 }
322 Some(y.atan2(x))
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_parse_direction() {
331 let content = "#1=IFCDIRECTION((0.5,0.25,0.75));";
333 let mut decoder = EntityDecoder::new(content);
334 let dir = decoder.decode_by_id(1).unwrap();
335
336 let v = parse_direction(&dir).unwrap();
337
338 assert!((v.x - 0.5).abs() < 1e-12);
339 assert!((v.y - 0.25).abs() < 1e-12);
340 assert!((v.z - 0.75).abs() < 1e-12);
341 }
342
343 #[test]
344 fn parse_cartesian_point_reads_coordinates_from_attribute_0() {
345 let content = "\
348#1=IFCCARTESIANPOINT((1.0,2.0,3.0));
349#2=IFCAXIS2PLACEMENT3D(#1,$,$);";
350 let mut decoder = EntityDecoder::new(content);
351 let placement = decoder.decode_by_id(2).unwrap();
352
353 let p = parse_cartesian_point(&placement, &mut decoder, 0).unwrap();
354
355 assert!((p.x - 1.0).abs() < 1e-12);
356 assert!((p.y - 2.0).abs() < 1e-12);
357 assert!((p.z - 3.0).abs() < 1e-12);
358 }
359
360 #[test]
361 fn parse_axis2_placement_3d_defaults_missing_axis_and_ref_direction() {
362 let content = "\
367#1=IFCCARTESIANPOINT((10.0,20.0,30.0));
368#2=IFCAXIS2PLACEMENT3D(#1);";
369 let mut decoder = EntityDecoder::new(content);
370 let placement = decoder.decode_by_id(2).unwrap();
371 assert_eq!(placement.attributes.len(), 1, "test fixture sanity check");
372
373 let m = parse_axis2_placement_3d(&placement, &mut decoder).unwrap();
374
375 assert!((m[(0, 3)] - 10.0).abs() < 1e-9);
377 assert!((m[(1, 3)] - 20.0).abs() < 1e-9);
378 assert!((m[(2, 3)] - 30.0).abs() < 1e-9);
379 assert!((m[(0, 2)] - 0.0).abs() < 1e-9);
381 assert!((m[(1, 2)] - 0.0).abs() < 1e-9);
382 assert!((m[(2, 2)] - 1.0).abs() < 1e-9);
383 assert!((m[(0, 0)] - 1.0).abs() < 1e-9);
385 assert!((m[(1, 0)] - 0.0).abs() < 1e-9);
386 assert!((m[(2, 0)] - 0.0).abs() < 1e-9);
387 }
388
389 #[test]
390 fn parse_axis2_placement_3d_defaults_ref_direction_when_only_axis_given() {
391 let content = "\
395#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
396#2=IFCDIRECTION((0.0,1.0,0.0));
397#3=IFCAXIS2PLACEMENT3D(#1,#2);";
398 let mut decoder = EntityDecoder::new(content);
399 let placement = decoder.decode_by_id(3).unwrap();
400 assert_eq!(placement.attributes.len(), 2, "test fixture sanity check");
401
402 let m = parse_axis2_placement_3d(&placement, &mut decoder).unwrap();
403
404 assert!((m[(0, 2)] - 0.0).abs() < 1e-9);
406 assert!((m[(1, 2)] - 1.0).abs() < 1e-9);
407 assert!((m[(2, 2)] - 0.0).abs() < 1e-9);
408 assert!((m[(0, 0)] - 1.0).abs() < 1e-9);
410 assert!((m[(1, 0)] - 0.0).abs() < 1e-9);
411 assert!((m[(2, 0)] - 0.0).abs() < 1e-9);
412 assert!((m[(0, 1)] - 0.0).abs() < 1e-9);
414 assert!((m[(1, 1)] - 0.0).abs() < 1e-9);
415 assert!((m[(2, 1)] - (-1.0)).abs() < 1e-9);
416 }
417
418 #[test]
419 fn parse_axis2_placement_3d_orthogonalizes_parallel_ref_direction_low_z() {
420 let content = "\
424#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
425#2=IFCDIRECTION((1.0,0.0,0.0));
426#3=IFCDIRECTION((1.0,0.0,0.0));
427#4=IFCAXIS2PLACEMENT3D(#1,#2,#3);";
428 let mut decoder = EntityDecoder::new(content);
429 let placement = decoder.decode_by_id(4).unwrap();
430
431 let m = parse_axis2_placement_3d(&placement, &mut decoder).unwrap();
432
433 assert!((m[(0, 0)] - 0.0).abs() < 1e-9);
436 assert!((m[(1, 0)] - 1.0).abs() < 1e-9);
437 assert!((m[(2, 0)] - 0.0).abs() < 1e-9);
438 assert!((m[(0, 1)] - 0.0).abs() < 1e-9);
440 assert!((m[(1, 1)] - 0.0).abs() < 1e-9);
441 assert!((m[(2, 1)] - 1.0).abs() < 1e-9);
442 }
443
444 #[test]
445 fn parse_axis2_placement_3d_orthogonalizes_parallel_ref_direction_high_z() {
446 let content = "\
450#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
451#2=IFCDIRECTION((0.0,0.0,1.0));
452#3=IFCDIRECTION((0.0,0.0,1.0));
453#4=IFCAXIS2PLACEMENT3D(#1,#2,#3);";
454 let mut decoder = EntityDecoder::new(content);
455 let placement = decoder.decode_by_id(4).unwrap();
456
457 let m = parse_axis2_placement_3d(&placement, &mut decoder).unwrap();
458
459 assert!((m[(0, 0)] - 0.0).abs() < 1e-9);
461 assert!((m[(1, 0)] - (-1.0)).abs() < 1e-9);
462 assert!((m[(2, 0)] - 0.0).abs() < 1e-9);
463 assert!((m[(0, 1)] - 1.0).abs() < 1e-9);
465 assert!((m[(1, 1)] - 0.0).abs() < 1e-9);
466 assert!((m[(2, 1)] - 0.0).abs() < 1e-9);
467 }
468
469 #[test]
470 fn rotation_angle_about_z_handles_identity_rotation_and_scale() {
471 let mut m = [0.0f64; 16];
473 m[0] = 1.0;
474 m[5] = 1.0;
475 m[10] = 1.0;
476 m[15] = 1.0;
477 assert!(rotation_angle_about_z(&m).unwrap().abs() < 1e-12);
478
479 let a = std::f64::consts::FRAC_PI_4;
482 let mut r = [0.0f64; 16];
483 r[0] = a.cos();
484 r[1] = a.sin();
485 r[4] = -a.sin();
486 r[5] = a.cos();
487 r[10] = 1.0;
488 r[15] = 1.0;
489 assert!((rotation_angle_about_z(&r).unwrap() - a).abs() < 1e-12);
490
491 let mut s = r;
493 for v in s.iter_mut().take(3) {
494 *v *= 3.0;
495 }
496 assert!((rotation_angle_about_z(&s).unwrap() - a).abs() < 1e-9);
497
498 assert!(rotation_angle_about_z(&[0.0f64; 16]).is_none());
500 }
501
502 #[test]
505 fn build_axis2_matrix_zero_axis_stays_finite() {
506 let m = build_axis2_matrix(
507 Point3::new(0.0, 0.0, 0.0),
508 Vector3::new(0.0, 0.0, 0.0),
509 Vector3::new(1.0, 0.0, 0.0),
510 );
511 assert!(m.iter().all(|v| v.is_finite()), "NaN in matrix: {m:?}");
512 }
513}