ifc_lite_geometry/analytic/
extrusion.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//! Authored `IfcExtrudedAreaSolid` parameters before occurrence placement.
6
7use super::{extract_analytic_profile, AnalyticProfile, AnalyticStatus};
8use crate::transform::parse_axis2_placement_3d;
9use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
10use serde::Serialize;
11
12/// One source extrusion in IFC file-length units and IFC Z-up. `Position`
13/// maps the profile's coordinates into the solid's local frame; a product or
14/// mapping transform is separate. No CSG or external void is applied here.
15#[derive(Debug, Clone, PartialEq, Serialize)]
16pub struct AnalyticExtrusion {
17    pub solid_id: u32,
18    #[serde(rename = "SweptArea")]
19    pub swept_area_id: Option<u32>,
20    pub profile: Option<AnalyticProfile>,
21    #[serde(rename = "Position")]
22    pub position_id: Option<u32>,
23    /// Matrix derived from `Position`, in raw IFC file-length units.
24    pub position_matrix: Option<[f64; 16]>,
25    #[serde(rename = "ExtrudedDirection")]
26    pub extruded_direction_id: Option<u32>,
27    /// Authored `IfcDirection.DirectionRatios`, before unit-vector normalization.
28    #[serde(rename = "DirectionRatios")]
29    pub direction_ratios: Option<[f64; 3]>,
30    /// Unit vector derived from `DirectionRatios` for geometric interpretation.
31    pub axis_unit_vector: Option<[f64; 3]>,
32    #[serde(rename = "Depth")]
33    pub depth: Option<f64>,
34    pub status: AnalyticStatus,
35}
36
37/// Return a complete exact source or an explicit unsupported reason. Even a
38/// malformed required attribute keeps its solid STEP ID in the result.
39pub fn extract_analytic_extrusion(
40    solid: &DecodedEntity, decoder: &mut EntityDecoder,
41) -> AnalyticExtrusion {
42    let mut result = AnalyticExtrusion {
43        solid_id: solid.id, swept_area_id: solid.get_ref(0), profile: None,
44        position_id: solid.get_ref(1), position_matrix: None,
45        extruded_direction_id: solid.get_ref(2), direction_ratios: None,
46        axis_unit_vector: None, depth: None, status: AnalyticStatus::Complete,
47    };
48    let parsed = parse_extrusion(solid, decoder, &mut result);
49    if let Err(reason) = parsed {
50        result.status = AnalyticStatus::Unsupported(reason);
51    }
52    result
53}
54
55fn parse_extrusion(
56    solid: &DecodedEntity, decoder: &mut EntityDecoder, result: &mut AnalyticExtrusion,
57) -> Result<(), String> {
58    match solid.ifc_type {
59        IfcType::IfcExtrudedAreaSolid => {}
60        IfcType::IfcExtrudedAreaSolidTapered =>
61            return Err("tapered extrusion has distinct start and end profiles".into()),
62        _ => return Err(format!("{} is not IfcExtrudedAreaSolid", solid.ifc_type.name())),
63    }
64    let area_id = result.swept_area_id.ok_or("missing SweptArea")?;
65    let area = decoder.decode_by_id(area_id)
66        .map_err(|error| format!("SweptArea #{area_id}: {error}"))?;
67    if !area.ifc_type.is_subtype_of(IfcType::IfcProfileDef) {
68        return Err(format!("SweptArea #{area_id} is not IfcProfileDef"));
69    }
70    let profile = extract_analytic_profile(&area, decoder);
71    if profile.profile_type.as_deref() != Some("AREA") {
72        let profile_type = profile.profile_type.clone().unwrap_or_else(|| "missing".into());
73        result.profile = Some(profile);
74        return Err(format!("SweptArea #{area_id} has ProfileType {profile_type}, expected AREA"));
75    }
76    if let AnalyticStatus::Unsupported(reason) = &profile.status {
77        let reason = format!("SweptArea #{area_id}: {reason}");
78        result.profile = Some(profile);
79        return Err(reason);
80    }
81    result.profile = Some(profile);
82
83    if let Some(position_attr) = solid.get(1).filter(|attribute| !attribute.is_null()) {
84        let id = position_attr.as_entity_ref().ok_or("invalid Position reference")?;
85        result.position_id = Some(id);
86        let position = decoder.decode_by_id(id)
87            .map_err(|error| format!("Position #{id}: {error}"))?;
88        if position.ifc_type != IfcType::IfcAxis2Placement3D {
89            return Err(format!("Position #{id} is not IfcAxis2Placement3D"));
90        }
91        let matrix = parse_axis2_placement_3d(&position, decoder)
92            .map_err(|error| format!("Position #{id}: {error}"))?;
93        if matrix.iter().any(|value| !value.is_finite()) {
94            return Err(format!("Position #{id} has non-finite coordinates"));
95        }
96        let mut values = [0.0; 16];
97        values.copy_from_slice(matrix.as_slice());
98        result.position_matrix = Some(values);
99    }
100
101    let direction_id = solid.get_ref(2).ok_or("missing ExtrudedDirection")?;
102    let direction = decoder.decode_by_id(direction_id)
103        .map_err(|error| format!("ExtrudedDirection #{direction_id}: {error}"))?;
104    if direction.ifc_type != IfcType::IfcDirection {
105        return Err(format!("ExtrudedDirection #{direction_id} is not IfcDirection"));
106    }
107    let ratios = direction.get_list(0).ok_or("ExtrudedDirection has no DirectionRatios")?;
108    if ratios.len() < 2 || ratios.len() > 3 {
109        return Err("ExtrudedDirection has invalid coordinate count".into());
110    }
111    let mut vector = [0.0; 3];
112    for (index, ratio) in ratios.iter().enumerate() {
113        vector[index] = ratio.as_float().ok_or("ExtrudedDirection has invalid ratio")?;
114    }
115    result.direction_ratios = Some(vector);
116    let norm = vector[0].hypot(vector[1]).hypot(vector[2]);
117    if !norm.is_finite() || norm <= 0.0 {
118        return Err("ExtrudedDirection is non-finite or zero".into());
119    }
120    let unit_axis = vector.map(|value| value / norm);
121    // IFC ValidExtrusionDirection: the sweep must not be perpendicular to the
122    // local Z axis of IfcSweptAreaSolid.Position.
123    if unit_axis[2] == 0.0 {
124        return Err("ExtrudedDirection is perpendicular to local Z".into());
125    }
126    result.axis_unit_vector = Some(unit_axis);
127
128    let depth = solid.get_float(3).ok_or("missing Depth")?;
129    if !depth.is_finite() || depth <= 0.0 {
130        return Err("invalid Depth".into());
131    }
132    result.depth = Some(depth);
133    Ok(())
134}
135
136#[cfg(test)]
137#[path = "extrusion_tests.rs"]
138mod tests;