1use crate::error::{GeometryError, GeometryResult};
31use crate::resource::axes::{base_axes_2d, base_axes_3d};
32use crate::resource::direction::resolve_unit;
33use crate::resource::point::cartesian_point_3d;
34use crate::slots::Slots;
35use crate::transform::Transform;
36use ifc_model::{Entity, EntityId, Model};
37
38pub(crate) mod slot {
40 pub const AXIS1: usize = 0;
42 pub const AXIS2: usize = 1;
44 pub const LOCAL_ORIGIN: usize = 2;
46 pub const SCALE: usize = 3;
48
49 pub const AXIS3: usize = 4;
53
54 pub const SCALE2_2D: usize = 4;
59
60 pub const SCALE2_3D: usize = 5;
62 pub const SCALE3_3D: usize = 6;
64}
65
66#[derive(Debug, Clone, Copy)]
72pub struct CartesianTransformationOperator<'m> {
73 slots: Slots<'m>,
74}
75
76impl<'m> CartesianTransformationOperator<'m> {
77 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
80 Self {
81 slots: Slots::new(id, entity),
82 }
83 }
84
85 pub fn id(&self) -> EntityId {
87 self.slots.id()
88 }
89
90 pub fn local_origin_ref(&self) -> GeometryResult<EntityId> {
92 self.slots.req_ref(slot::LOCAL_ORIGIN, "LocalOrigin")
93 }
94
95 pub fn local_origin(&self, model: &'m Model) -> GeometryResult<[f64; 3]> {
97 cartesian_point_3d(model, self.id(), self.local_origin_ref()?)
98 }
99
100 pub fn scale_attribute(&self) -> Option<f64> {
105 self.slots.opt_f64(slot::SCALE)
106 }
107
108 pub fn scale(&self) -> GeometryResult<f64> {
115 let value = self.scale_attribute().unwrap_or(1.0);
116 self.checked_scale(value, "Scale")
117 }
118
119 pub fn axis1(&self, model: &'m Model) -> GeometryResult<Option<[f64; 3]>> {
121 self.optional_direction(model, slot::AXIS1)
122 }
123
124 pub fn axis2(&self, model: &'m Model) -> GeometryResult<Option<[f64; 3]>> {
130 self.optional_direction(model, slot::AXIS2)
131 }
132
133 fn optional_direction(
135 &self,
136 model: &'m Model,
137 index: usize,
138 ) -> GeometryResult<Option<[f64; 3]>> {
139 match self.slots.opt_ref(index) {
140 Some(id) => resolve_unit(model, self.id(), id).map(Some),
141 None => Ok(None),
142 }
143 }
144
145 fn checked_scale(&self, value: f64, attribute: &str) -> GeometryResult<f64> {
147 if value > 0.0 {
148 Ok(value)
149 } else {
150 Err(self.slots.degenerate(format!(
151 "{attribute} is {value}, but the schema requires a scale greater than zero"
152 )))
153 }
154 }
155}
156
157#[derive(Debug, Clone, Copy)]
159pub struct CartesianTransformationOperator2D<'m> {
160 base: CartesianTransformationOperator<'m>,
161}
162
163impl<'m> CartesianTransformationOperator2D<'m> {
164 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
166 Self {
167 base: CartesianTransformationOperator::new(id, entity),
168 }
169 }
170
171 pub fn base(&self) -> CartesianTransformationOperator<'m> {
173 self.base
174 }
175
176 pub fn id(&self) -> EntityId {
178 self.base.id()
179 }
180
181 pub fn transform(&self, model: &'m Model) -> GeometryResult<Transform> {
183 let scale = self.base.scale()?;
184 self.scaled_transform(model, [scale, scale])
185 }
186
187 fn scaled_transform(&self, model: &'m Model, factors: [f64; 2]) -> GeometryResult<Transform> {
192 let origin = self.base.local_origin(model)?;
193 let axis1 = self.base.axis1(model)?;
194 let axis2 = self.base.axis2(model)?;
195 let frame = base_axes_2d(origin, axis1, axis2).ok_or_else(|| {
196 self.base
197 .slots
198 .degenerate("Axis1 and Axis2 do not define a 2D frame")
199 })?;
200 Ok(frame.scaled_nonuniform([factors[0], factors[1], 1.0]))
201 }
202}
203
204#[derive(Debug, Clone, Copy)]
206pub struct CartesianTransformationOperator2DnonUniform<'m> {
207 inner: CartesianTransformationOperator2D<'m>,
208}
209
210impl<'m> CartesianTransformationOperator2DnonUniform<'m> {
211 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
213 Self {
214 inner: CartesianTransformationOperator2D::new(id, entity),
215 }
216 }
217
218 pub fn id(&self) -> EntityId {
220 self.inner.id()
221 }
222
223 pub fn base(&self) -> CartesianTransformationOperator<'m> {
225 self.inner.base()
226 }
227
228 pub fn scale2(&self) -> GeometryResult<f64> {
232 let base = self.base();
233 let value = base.slots.opt_f64(slot::SCALE2_2D).unwrap_or(base.scale()?);
234 base.checked_scale(value, "Scale2")
235 }
236
237 pub fn transform(&self, model: &'m Model) -> GeometryResult<Transform> {
239 let factors = [self.base().scale()?, self.scale2()?];
240 self.inner.scaled_transform(model, factors)
241 }
242}
243
244#[derive(Debug, Clone, Copy)]
246pub struct CartesianTransformationOperator3D<'m> {
247 base: CartesianTransformationOperator<'m>,
248}
249
250impl<'m> CartesianTransformationOperator3D<'m> {
251 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
253 Self {
254 base: CartesianTransformationOperator::new(id, entity),
255 }
256 }
257
258 pub fn base(&self) -> CartesianTransformationOperator<'m> {
260 self.base
261 }
262
263 pub fn id(&self) -> EntityId {
265 self.base.id()
266 }
267
268 pub fn axis3(&self, model: &'m Model) -> GeometryResult<Option<[f64; 3]>> {
270 self.base.optional_direction(model, slot::AXIS3)
271 }
272
273 pub fn transform(&self, model: &'m Model) -> GeometryResult<Transform> {
275 let scale = self.base.scale()?;
276 self.scaled_transform(model, [scale; 3])
277 }
278
279 fn scaled_transform(&self, model: &'m Model, factors: [f64; 3]) -> GeometryResult<Transform> {
281 let origin = self.base.local_origin(model)?;
282 let axis1 = self.base.axis1(model)?;
283 let axis2 = self.base.axis2(model)?;
284 let axis3 = self.axis3(model)?;
285 let frame = base_axes_3d(origin, axis1, axis2, axis3).ok_or_else(|| {
286 self.base
287 .slots
288 .degenerate("Axis1 and Axis3 are parallel, so they define no frame")
289 })?;
290 Ok(frame.scaled_nonuniform(factors))
291 }
292}
293
294#[derive(Debug, Clone, Copy)]
296pub struct CartesianTransformationOperator3DnonUniform<'m> {
297 inner: CartesianTransformationOperator3D<'m>,
298}
299
300impl<'m> CartesianTransformationOperator3DnonUniform<'m> {
301 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
303 Self {
304 inner: CartesianTransformationOperator3D::new(id, entity),
305 }
306 }
307
308 pub fn id(&self) -> EntityId {
310 self.inner.id()
311 }
312
313 pub fn base(&self) -> CartesianTransformationOperator<'m> {
315 self.inner.base()
316 }
317
318 pub fn scale2(&self) -> GeometryResult<f64> {
320 self.derived_scale(slot::SCALE2_3D, "Scale2")
321 }
322
323 pub fn scale3(&self) -> GeometryResult<f64> {
325 self.derived_scale(slot::SCALE3_3D, "Scale3")
326 }
327
328 pub fn transform(&self, model: &'m Model) -> GeometryResult<Transform> {
330 let factors = [self.base().scale()?, self.scale2()?, self.scale3()?];
331 self.inner.scaled_transform(model, factors)
332 }
333
334 fn derived_scale(&self, index: usize, attribute: &str) -> GeometryResult<f64> {
336 let base = self.base();
337 let value = base.slots.opt_f64(index).unwrap_or(base.scale()?);
338 base.checked_scale(value, attribute)
339 }
340}
341
342pub fn operator_transform(
350 model: &Model,
351 id: EntityId,
352 entity: &Entity,
353) -> GeometryResult<Transform> {
354 match entity.type_name.to_ascii_uppercase().as_str() {
355 "IFCCARTESIANTRANSFORMATIONOPERATOR3D" => {
356 CartesianTransformationOperator3D::new(id, entity).transform(model)
357 }
358 "IFCCARTESIANTRANSFORMATIONOPERATOR3DNONUNIFORM" => {
359 CartesianTransformationOperator3DnonUniform::new(id, entity).transform(model)
360 }
361 "IFCCARTESIANTRANSFORMATIONOPERATOR2D" => {
362 CartesianTransformationOperator2D::new(id, entity).transform(model)
363 }
364 "IFCCARTESIANTRANSFORMATIONOPERATOR2DNONUNIFORM" => {
365 CartesianTransformationOperator2DnonUniform::new(id, entity).transform(model)
366 }
367 other => Err(GeometryError::WrongEntityType {
368 entity: id,
369 actual: other.to_string(),
370 expected: "IfcCartesianTransformationOperator",
371 }),
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::error::GeometryError;
379 use ifc_model::Value;
380
381 fn coords(values: &[f64]) -> Value {
382 Value::List(values.iter().copied().map(Value::Real).collect())
383 }
384
385 fn model() -> Model {
387 let mut model = Model::new();
388 model.insert(
389 EntityId(1),
390 Entity::new("IFCCARTESIANPOINT", vec![coords(&[1.0, 2.0, 3.0])]),
391 );
392 model.insert(
393 EntityId(2),
394 Entity::new("IFCDIRECTION", vec![coords(&[1.0, 0.0, 0.0])]),
395 );
396 model.insert(
397 EntityId(3),
398 Entity::new("IFCDIRECTION", vec![coords(&[0.0, 1.0, 0.0])]),
399 );
400 model.insert(
401 EntityId(4),
402 Entity::new("IFCDIRECTION", vec![coords(&[0.0, 0.0, 1.0])]),
403 );
404 model
405 }
406
407 fn operator_3d(scale: Value) -> Entity {
410 Entity::new(
411 "IFCCARTESIANTRANSFORMATIONOPERATOR3D",
412 vec![
413 Value::Null,
414 Value::Null,
415 Value::Ref(EntityId(1)),
416 scale,
417 Value::Null,
418 ],
419 )
420 }
421
422 fn close(a: [f64; 3], b: [f64; 3]) -> bool {
423 a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-9)
424 }
425
426 #[test]
428 fn absent_scale_defaults_to_one_not_zero() {
429 let e = operator_3d(Value::Null);
430 let op = CartesianTransformationOperator::new(EntityId(9), &e);
431 assert_eq!(op.scale_attribute(), None);
432 assert_eq!(op.scale().unwrap(), 1.0);
433 }
434
435 #[test]
438 fn local_origin_is_read_after_the_two_optional_axes() {
439 let model = model();
440 let e = operator_3d(Value::Null);
441 let op = CartesianTransformationOperator::new(EntityId(9), &e);
442 assert_eq!(op.local_origin(&model).unwrap(), [1.0, 2.0, 3.0]);
443 }
444
445 #[test]
446 fn a_uniform_operator_scales_every_axis_by_scale() {
447 let model = model();
448 let e = operator_3d(Value::Real(2.0));
449 let t = CartesianTransformationOperator3D::new(EntityId(9), &e)
450 .transform(&model)
451 .unwrap();
452 assert!(close(t.basis[0], [2.0, 0.0, 0.0]));
453 assert!(close(t.basis[1], [0.0, 2.0, 0.0]));
454 assert!(close(t.basis[2], [0.0, 0.0, 2.0]));
455 assert_eq!(t.origin, [1.0, 2.0, 3.0]);
456 }
457
458 #[test]
460 fn axis1_is_the_local_x_and_axis3_the_local_z() {
461 let model = model();
462 let e = Entity::new(
463 "IFCCARTESIANTRANSFORMATIONOPERATOR3D",
464 vec![
465 Value::Ref(EntityId(3)), Value::Null,
467 Value::Ref(EntityId(1)),
468 Value::Null,
469 Value::Ref(EntityId(4)), ],
471 );
472 let t = CartesianTransformationOperator3D::new(EntityId(9), &e)
473 .transform(&model)
474 .unwrap();
475 assert!(close(t.basis[0], [0.0, 1.0, 0.0]), "got {:?}", t.basis[0]);
476 assert!(close(t.basis[2], [0.0, 0.0, 1.0]));
477 }
478
479 #[test]
482 fn nonuniform_secondary_scales_default_to_scale_not_to_one() {
483 let model = model();
484 let e = Entity::new(
485 "IFCCARTESIANTRANSFORMATIONOPERATOR3DNONUNIFORM",
486 vec![
487 Value::Null,
488 Value::Null,
489 Value::Ref(EntityId(1)),
490 Value::Real(3.0),
491 Value::Null,
492 Value::Null, Value::Null, ],
495 );
496 let op = CartesianTransformationOperator3DnonUniform::new(EntityId(9), &e);
497 assert_eq!(op.scale2().unwrap(), 3.0);
498 assert_eq!(op.scale3().unwrap(), 3.0);
499 let t = op.transform(&model).unwrap();
500 assert!(close(t.basis[1], [0.0, 3.0, 0.0]));
501 assert!(close(t.basis[2], [0.0, 0.0, 3.0]));
502 }
503
504 #[test]
505 fn nonuniform_scales_are_applied_per_axis_when_given() {
506 let model = model();
507 let e = Entity::new(
508 "IFCCARTESIANTRANSFORMATIONOPERATOR3DNONUNIFORM",
509 vec![
510 Value::Null,
511 Value::Null,
512 Value::Ref(EntityId(1)),
513 Value::Real(2.0),
514 Value::Null,
515 Value::Real(5.0),
516 Value::Real(7.0),
517 ],
518 );
519 let t = CartesianTransformationOperator3DnonUniform::new(EntityId(9), &e)
520 .transform(&model)
521 .unwrap();
522 assert!(close(t.basis[0], [2.0, 0.0, 0.0]));
523 assert!(close(t.basis[1], [0.0, 5.0, 0.0]));
524 assert!(close(t.basis[2], [0.0, 0.0, 7.0]));
525 }
526
527 #[test]
530 fn two_d_nonuniform_reads_scale2_one_slot_earlier_than_the_three_d_one() {
531 let model = model();
532 let e = Entity::new(
533 "IFCCARTESIANTRANSFORMATIONOPERATOR2DNONUNIFORM",
534 vec![
535 Value::Null,
536 Value::Null,
537 Value::Ref(EntityId(1)),
538 Value::Real(2.0),
539 Value::Real(6.0), ],
541 );
542 let op = CartesianTransformationOperator2DnonUniform::new(EntityId(9), &e);
543 assert_eq!(op.scale2().unwrap(), 6.0);
544 let t = op.transform(&model).unwrap();
545 assert!(close(t.basis[0], [2.0, 0.0, 0.0]));
546 assert!(close(t.basis[1], [0.0, 6.0, 0.0]));
547 }
548
549 #[test]
551 fn two_d_operator_leaves_the_z_axis_unscaled() {
552 let model = model();
553 let e = Entity::new(
554 "IFCCARTESIANTRANSFORMATIONOPERATOR2D",
555 vec![
556 Value::Null,
557 Value::Null,
558 Value::Ref(EntityId(1)),
559 Value::Real(4.0),
560 ],
561 );
562 let t = CartesianTransformationOperator2D::new(EntityId(9), &e)
563 .transform(&model)
564 .unwrap();
565 assert!(close(t.basis[0], [4.0, 0.0, 0.0]));
566 assert!(close(t.basis[1], [0.0, 4.0, 0.0]));
567 assert!(close(t.basis[2], [0.0, 0.0, 1.0]), "got {:?}", t.basis[2]);
568 }
569
570 #[test]
573 fn two_d_y_axis_is_the_orthogonal_complement_of_axis1() {
574 let mut model = model();
575 model.insert(
576 EntityId(5),
577 Entity::new("IFCDIRECTION", vec![coords(&[0.0, 1.0])]),
578 );
579 let e = Entity::new(
580 "IFCCARTESIANTRANSFORMATIONOPERATOR2D",
581 vec![
582 Value::Ref(EntityId(5)),
583 Value::Null,
584 Value::Ref(EntityId(1)),
585 Value::Null,
586 ],
587 );
588 let t = CartesianTransformationOperator2D::new(EntityId(9), &e)
589 .transform(&model)
590 .unwrap();
591 assert!(close(t.basis[0], [0.0, 1.0, 0.0]));
592 assert!(close(t.basis[1], [-1.0, 0.0, 0.0]), "got {:?}", t.basis[1]);
593 }
594
595 #[test]
598 fn an_opposing_axis2_flips_the_derived_y_axis() {
599 let mut model = model();
600 model.insert(
601 EntityId(6),
602 Entity::new("IFCDIRECTION", vec![coords(&[0.0, -1.0])]),
603 );
604 model.insert(
605 EntityId(7),
606 Entity::new("IFCDIRECTION", vec![coords(&[1.0, 0.0])]),
607 );
608 let e = Entity::new(
609 "IFCCARTESIANTRANSFORMATIONOPERATOR2D",
610 vec![
611 Value::Ref(EntityId(7)), Value::Ref(EntityId(6)), Value::Ref(EntityId(1)),
614 Value::Null,
615 ],
616 );
617 let t = CartesianTransformationOperator2D::new(EntityId(9), &e)
618 .transform(&model)
619 .unwrap();
620 assert!(close(t.basis[1], [0.0, -1.0, 0.0]), "got {:?}", t.basis[1]);
621 }
622
623 #[test]
626 fn an_opposing_axis2_flips_handedness_in_three_d_too() {
627 let mut model = model();
628 model.insert(
629 EntityId(8),
630 Entity::new("IFCDIRECTION", vec![coords(&[0.0, -1.0, 0.0])]),
631 );
632 let e = Entity::new(
633 "IFCCARTESIANTRANSFORMATIONOPERATOR3D",
634 vec![
635 Value::Ref(EntityId(2)),
636 Value::Ref(EntityId(8)),
637 Value::Ref(EntityId(1)),
638 Value::Null,
639 Value::Ref(EntityId(4)),
640 ],
641 );
642 let t = CartesianTransformationOperator3D::new(EntityId(9), &e)
643 .transform(&model)
644 .unwrap();
645 assert!(close(t.basis[1], [0.0, -1.0, 0.0]), "got {:?}", t.basis[1]);
646 }
647
648 #[test]
651 fn zero_scale_is_degenerate() {
652 let e = operator_3d(Value::Real(0.0));
653 let err = CartesianTransformationOperator::new(EntityId(9), &e)
654 .scale()
655 .unwrap_err();
656 assert!(matches!(err, GeometryError::Degenerate { .. }), "{err}");
657 }
658
659 #[test]
660 fn negative_scale_is_degenerate() {
661 let e = operator_3d(Value::Real(-1.0));
662 assert!(CartesianTransformationOperator::new(EntityId(9), &e)
663 .scale()
664 .is_err());
665 }
666
667 #[test]
668 fn a_negative_secondary_scale_names_that_attribute() {
669 let e = Entity::new(
670 "IFCCARTESIANTRANSFORMATIONOPERATOR3DNONUNIFORM",
671 vec![
672 Value::Null,
673 Value::Null,
674 Value::Ref(EntityId(1)),
675 Value::Real(1.0),
676 Value::Null,
677 Value::Real(-2.0),
678 Value::Null,
679 ],
680 );
681 let err = CartesianTransformationOperator3DnonUniform::new(EntityId(9), &e)
682 .scale2()
683 .unwrap_err();
684 assert!(err.to_string().contains("Scale2"), "got: {err}");
685 }
686
687 #[test]
689 fn a_zero_length_axis_is_degenerate_rather_than_nan() {
690 let mut model = model();
691 model.insert(
692 EntityId(9),
693 Entity::new("IFCDIRECTION", vec![coords(&[0.0, 0.0, 0.0])]),
694 );
695 let e = Entity::new(
696 "IFCCARTESIANTRANSFORMATIONOPERATOR3D",
697 vec![
698 Value::Null,
699 Value::Null,
700 Value::Ref(EntityId(1)),
701 Value::Null,
702 Value::Ref(EntityId(9)),
703 ],
704 );
705 let err = CartesianTransformationOperator3D::new(EntityId(20), &e)
706 .transform(&model)
707 .unwrap_err();
708 assert!(matches!(err, GeometryError::Degenerate { .. }), "{err}");
709 }
710
711 #[test]
713 fn axis1_parallel_to_axis3_is_degenerate() {
714 let model = model();
715 let e = Entity::new(
716 "IFCCARTESIANTRANSFORMATIONOPERATOR3D",
717 vec![
718 Value::Ref(EntityId(4)),
719 Value::Null,
720 Value::Ref(EntityId(1)),
721 Value::Null,
722 Value::Ref(EntityId(4)),
723 ],
724 );
725 assert!(CartesianTransformationOperator3D::new(EntityId(20), &e)
726 .transform(&model)
727 .is_err());
728 }
729
730 #[test]
731 fn a_missing_local_origin_names_the_entity_and_attribute() {
732 let e = Entity::new("IFCCARTESIANTRANSFORMATIONOPERATOR3D", vec![]);
733 let err = CartesianTransformationOperator::new(EntityId(42), &e)
734 .local_origin_ref()
735 .unwrap_err();
736 assert!(err.to_string().contains("#42"), "got: {err}");
737 assert!(err.to_string().contains("LocalOrigin"), "got: {err}");
738 }
739
740 #[test]
742 fn an_operator_without_axes_is_a_pure_translation_and_scale() {
743 let model = model();
744 let e = operator_3d(Value::Null);
745 let t = CartesianTransformationOperator3D::new(EntityId(9), &e)
746 .transform(&model)
747 .unwrap();
748 assert_eq!(t, Transform::translation([1.0, 2.0, 3.0]));
749 }
750
751 #[test]
753 fn a_two_d_operator_with_only_axis2_derives_x_from_it() {
754 let mut model = model();
755 model.insert(
756 EntityId(5),
757 Entity::new("IFCDIRECTION", vec![coords(&[0.0, 1.0])]),
758 );
759 let e = Entity::new(
760 "IFCCARTESIANTRANSFORMATIONOPERATOR2D",
761 vec![
762 Value::Null,
763 Value::Ref(EntityId(5)),
764 Value::Ref(EntityId(1)),
765 Value::Null,
766 ],
767 );
768 let t = CartesianTransformationOperator2D::new(EntityId(9), &e)
769 .transform(&model)
770 .unwrap();
771 assert!(close(t.basis[0], [1.0, 0.0, 0.0]), "got {:?}", t.basis[0]);
772 assert!(close(t.basis[1], [0.0, 1.0, 0.0]), "got {:?}", t.basis[1]);
773 }
774}