Skip to main content

ifc_lite_geometry/processors/
extrusion_tapered.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! ExtrudedAreaSolidTapered processor — lofted extrusion between two profiles.
6//!
7//! Adds support for `IfcExtrudedAreaSolidTapered`, a subtype of
8//! `IfcExtrudedAreaSolid` with one extra attribute (`EndSweptArea`, attr 4).
9//! The cross-section linearly transitions from `SweptArea` at the base to
10//! `EndSweptArea` at `Depth` along `ExtrudedDirection`.
11
12use crate::{
13    extrusion::{apply_transform, extrude_profile, extrude_profile_lofted},
14    profiles::ProfileProcessor,
15    Error, Mesh, Result, TessellationQuality, Vector3,
16};
17use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
18use nalgebra::Matrix4;
19
20use super::helpers::parse_axis2_placement_3d;
21use crate::router::GeometryProcessor;
22
23pub struct ExtrudedAreaSolidTaperedProcessor {
24    profile_processor: ProfileProcessor,
25}
26
27impl ExtrudedAreaSolidTaperedProcessor {
28    pub fn new(schema: IfcSchema) -> Self {
29        Self {
30            profile_processor: ProfileProcessor::new(schema),
31        }
32    }
33}
34
35impl GeometryProcessor for ExtrudedAreaSolidTaperedProcessor {
36    fn process(
37        &self,
38        entity: &DecodedEntity,
39        decoder: &mut EntityDecoder,
40        _schema: &IfcSchema,
41        quality: TessellationQuality,
42    ) -> Result<Mesh> {
43        // IfcExtrudedAreaSolidTapered attributes (inherits IfcExtrudedAreaSolid):
44        // 0: SweptArea       (start profile, IfcProfileDef)
45        // 1: Position        (IfcAxis2Placement3D)
46        // 2: ExtrudedDirection (IfcDirection)
47        // 3: Depth           (IfcPositiveLengthMeasure)
48        // 4: EndSweptArea    (end profile, IfcProfileDef)
49
50        let start_attr = entity.get(0).ok_or_else(|| {
51            Error::geometry("ExtrudedAreaSolidTapered missing SweptArea".to_string())
52        })?;
53        let start_entity = decoder
54            .resolve_ref(start_attr)?
55            .ok_or_else(|| Error::geometry("Failed to resolve SweptArea".to_string()))?;
56        let start_profile = self
57            .profile_processor
58            .process(&start_entity, decoder, quality)?;
59        if start_profile.outer.is_empty() {
60            return Ok(Mesh::new());
61        }
62
63        let direction_attr = entity.get(2).ok_or_else(|| {
64            Error::geometry("ExtrudedAreaSolidTapered missing ExtrudedDirection".to_string())
65        })?;
66        let direction_entity = decoder
67            .resolve_ref(direction_attr)?
68            .ok_or_else(|| Error::geometry("Failed to resolve ExtrudedDirection".to_string()))?;
69        if direction_entity.ifc_type != IfcType::IfcDirection {
70            return Err(Error::geometry(format!(
71                "Expected IfcDirection, got {}",
72                direction_entity.ifc_type
73            )));
74        }
75
76        use ifc_lite_core::AttributeValue;
77        let ratios_attr = direction_entity
78            .get(0)
79            .ok_or_else(|| Error::geometry("IfcDirection missing ratios".to_string()))?;
80        let ratios = ratios_attr
81            .as_list()
82            .ok_or_else(|| Error::geometry("Expected ratio list".to_string()))?;
83        let dir_x = ratios
84            .first()
85            .and_then(|v: &AttributeValue| v.as_float())
86            .unwrap_or(0.0);
87        let dir_y = ratios
88            .get(1)
89            .and_then(|v: &AttributeValue| v.as_float())
90            .unwrap_or(0.0);
91        let dir_z = ratios
92            .get(2)
93            .and_then(|v: &AttributeValue| v.as_float())
94            .unwrap_or(1.0);
95        let direction = Vector3::new(dir_x, dir_y, dir_z);
96        if direction.norm_squared() <= f64::EPSILON {
97            return Err(Error::geometry(
98                "ExtrudedAreaSolidTapered has zero-length ExtrudedDirection".to_string(),
99            ));
100        }
101        let local_direction = direction.normalize();
102
103        let depth = entity.get_float(3).ok_or_else(|| {
104            Error::geometry("ExtrudedAreaSolidTapered missing Depth".to_string())
105        })?;
106
107        let pos_transform = if let Some(pos_attr) = entity.get(1) {
108            if !pos_attr.is_null() {
109                if let Some(pos_entity) = decoder.resolve_ref(pos_attr)? {
110                    if pos_entity.ifc_type == IfcType::IfcAxis2Placement3D {
111                        Some(parse_axis2_placement_3d(&pos_entity, decoder)?)
112                    } else {
113                        None
114                    }
115                } else {
116                    None
117                }
118            } else {
119                None
120            }
121        } else {
122            None
123        };
124
125        // Same Z-aligned vs shear branch as ExtrudedAreaSolidProcessor — see
126        // processors/extrusion.rs for the rationale.
127        let is_local_z_aligned =
128            local_direction.x.abs() < 0.001 && local_direction.y.abs() < 0.001;
129        let transform = if is_local_z_aligned {
130            if local_direction.z < 0.0 {
131                Some(Matrix4::new_translation(&Vector3::new(0.0, 0.0, -depth)))
132            } else {
133                None
134            }
135        } else {
136            let mut shear_mat = Matrix4::identity();
137            shear_mat[(0, 2)] = local_direction.x;
138            shear_mat[(1, 2)] = local_direction.y;
139            shear_mat[(2, 2)] = local_direction.z;
140            Some(shear_mat)
141        };
142
143        // Resolve EndSweptArea (attr 4). If missing, unresolvable, or its
144        // profile fails to process, fall back to a uniform extrusion so the
145        // element still renders rather than dropping geometry entirely.
146        let end_profile_opt = match entity.get(4) {
147            Some(attr) if !attr.is_null() => match decoder.resolve_ref(attr)? {
148                Some(end_entity) => {
149                    match self.profile_processor.process(&end_entity, decoder, quality) {
150                        Ok(p) if !p.outer.is_empty() => Some(p),
151                        Ok(_) => None,
152                        Err(_) => None,
153                    }
154                }
155                None => None,
156            },
157            _ => None,
158        };
159
160        let mut mesh = match end_profile_opt {
161            Some(end_profile) => {
162                extrude_profile_lofted(&start_profile, &end_profile, depth, transform)?
163            }
164            None => extrude_profile(&start_profile, depth, transform)?,
165        };
166
167        if let Some(pos) = pos_transform {
168            apply_transform(&mut mesh, &pos);
169        }
170
171        Ok(mesh)
172    }
173
174    fn supported_types(&self) -> Vec<IfcType> {
175        vec![IfcType::IfcExtrudedAreaSolidTapered]
176    }
177}