Skip to main content

ifc_lite_geometry/processors/swept/
revolved.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
5use crate::{
6    extrusion::apply_transform, profiles::ProfileProcessor, scale_segments, Error, Mesh, Point3,
7    Result, TessellationQuality, Vector3,
8};
9use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
10use nalgebra::Matrix4;
11
12use super::super::helpers::parse_axis2_placement_3d;
13use super::super::tessellated::PolygonalFaceSetProcessor;
14use crate::router::GeometryProcessor;
15
16/// RevolvedAreaSolid processor
17/// Handles IfcRevolvedAreaSolid - rotates a 2D profile around an axis
18pub struct RevolvedAreaSolidProcessor {
19    profile_processor: ProfileProcessor,
20}
21
22impl RevolvedAreaSolidProcessor {
23    pub fn new(schema: IfcSchema) -> Self {
24        Self {
25            profile_processor: ProfileProcessor::new(schema),
26        }
27    }
28}
29
30impl GeometryProcessor for RevolvedAreaSolidProcessor {
31    fn process(
32        &self,
33        entity: &DecodedEntity,
34        decoder: &mut EntityDecoder,
35        _schema: &IfcSchema,
36        quality: TessellationQuality,
37    ) -> Result<Mesh> {
38        // IfcRevolvedAreaSolid attributes (inherits IfcSweptAreaSolid):
39        // 0: SweptArea (IfcProfileDef) - 2D profile in xy plane of Position
40        // 1: Position (IfcAxis2Placement3D) - solid's local coord system
41        // 2: Axis (IfcAxis1Placement) - revolution axis in xy plane of Position
42        // 3: Angle (IfcPlaneAngleMeasure) - revolution angle in the project's
43        //    PLANEANGLEUNIT (radians for SI files, degrees for files that
44        //    declare a DEGREE conversion-based unit). Scaled to radians below
45        //    via decoder.plane_angle_to_radians() — see issue #820 for the
46        //    same class of bug on IfcTrimmedCurve parameters.
47
48        let profile_attr = entity
49            .get(0)
50            .ok_or_else(|| Error::geometry("RevolvedAreaSolid missing SweptArea".to_string()))?;
51        let profile = decoder
52            .resolve_ref(profile_attr)?
53            .ok_or_else(|| Error::geometry("Failed to resolve SweptArea".to_string()))?;
54
55        // Position transform: maps Position-local coords -> object coords.
56        // Optional in some files; default to identity.
57        let position_transform = if let Some(pos_attr) = entity.get(1) {
58            if !pos_attr.is_null() {
59                if let Some(pos_entity) = decoder.resolve_ref(pos_attr)? {
60                    parse_axis2_placement_3d(&pos_entity, decoder)?
61                } else {
62                    Matrix4::identity()
63                }
64            } else {
65                Matrix4::identity()
66            }
67        } else {
68            Matrix4::identity()
69        };
70
71        let axis_attr = entity
72            .get(2)
73            .ok_or_else(|| Error::geometry("RevolvedAreaSolid missing Axis".to_string()))?;
74        let axis_placement = decoder
75            .resolve_ref(axis_attr)?
76            .ok_or_else(|| Error::geometry("Failed to resolve Axis".to_string()))?;
77
78        let angle = entity
79            .get_float(3)
80            .ok_or_else(|| Error::geometry("RevolvedAreaSolid missing Angle".to_string()))?
81            * decoder.plane_angle_to_radians();
82
83        let profile_2d = self.profile_processor.process(&profile, decoder, quality)?;
84        if profile_2d.outer.is_empty() {
85            return Ok(Mesh::new());
86        }
87
88        // IfcAxis1Placement: 0=Location (IfcCartesianPoint), 1=Axis (IfcDirection, optional)
89        let axis_location = {
90            let loc_attr = axis_placement
91                .get(0)
92                .ok_or_else(|| Error::geometry("Axis1Placement missing Location".to_string()))?;
93            let loc = decoder
94                .resolve_ref(loc_attr)?
95                .ok_or_else(|| Error::geometry("Failed to resolve axis location".to_string()))?;
96            let coords = loc
97                .get(0)
98                .and_then(|v| v.as_list())
99                .ok_or_else(|| Error::geometry("Axis location missing coordinates".to_string()))?;
100            Point3::new(
101                coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
102                coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
103                coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
104            )
105        };
106
107        let axis_direction = {
108            if let Some(dir_attr) = axis_placement.get(1) {
109                if !dir_attr.is_null() {
110                    let dir = decoder.resolve_ref(dir_attr)?.ok_or_else(|| {
111                        Error::geometry("Failed to resolve axis direction".to_string())
112                    })?;
113                    let coords = dir.get(0).and_then(|v| v.as_list()).ok_or_else(|| {
114                        Error::geometry("Axis direction missing coordinates".to_string())
115                    })?;
116                    let raw = Vector3::new(
117                        coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
118                        coords.get(1).and_then(|v| v.as_float()).unwrap_or(1.0),
119                        coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
120                    );
121                    if raw.norm() < 1e-12 {
122                        Vector3::new(0.0, 1.0, 0.0)
123                    } else {
124                        raw.normalize()
125                    }
126                } else {
127                    Vector3::new(0.0, 1.0, 0.0)
128                }
129            } else {
130                Vector3::new(0.0, 1.0, 0.0)
131            }
132        };
133
134        let full_circle = angle.abs() >= std::f64::consts::PI * 1.99;
135        // 24 segments for a full revolve at Medium; ~12 per 180° (min 8) for a
136        // partial arc. Both scaled by quality; the high upper bound preserves
137        // the original uncapped partial-arc count at Medium.
138        let segments = if full_circle {
139            scale_segments(24, 8, 96, quality)
140        } else {
141            let base = (angle.abs() / std::f64::consts::PI * 12.0).ceil() as usize;
142            scale_segments(base, 8, 4096, quality)
143        };
144
145        let profile_points = &profile_2d.outer;
146        let num_profile_points = profile_points.len();
147
148        let ring_count = if full_circle { segments } else { segments + 1 };
149        let mut positions = Vec::with_capacity(ring_count * num_profile_points * 3);
150        let mut indices = Vec::new();
151
152        // Rotate each profile vertex around the axis line in Position-local coords.
153        for i in 0..ring_count {
154            let t = if full_circle {
155                std::f64::consts::TAU * i as f64 / segments as f64
156            } else {
157                angle * i as f64 / segments as f64
158            };
159
160            let cos_t = t.cos();
161            let sin_t = t.sin();
162            let k = axis_direction;
163
164            for p2d in profile_points {
165                // Lift profile vertex into Position-local 3D (xy plane, z=0)
166                let p_local = Point3::new(p2d.x, p2d.y, 0.0);
167
168                // Decompose v = (p_local - axis_location) into parallel + perpendicular
169                // to the axis, then rotate only the perpendicular component by t.
170                let v = p_local - axis_location;
171                let v_par_len = v.dot(&k);
172                let v_par = k * v_par_len;
173                let v_perp = v - v_par;
174                let v_perp_rot = v_perp * cos_t + k.cross(&v_perp) * sin_t;
175
176                let pos_local = axis_location + v_par + v_perp_rot;
177
178                positions.push(pos_local.x as f32);
179                positions.push(pos_local.y as f32);
180                positions.push(pos_local.z as f32);
181            }
182        }
183
184        // Side quads. The last ring connects back to the first only when the
185        // sweep closes the loop (full revolution).
186        let segment_quads = segments;
187        for i in 0..segment_quads {
188            let ring_a = i;
189            let ring_b = (i + 1) % ring_count;
190            for j in 0..num_profile_points {
191                let j_next = (j + 1) % num_profile_points;
192                let a = (ring_a * num_profile_points + j) as u32;
193                let b = (ring_b * num_profile_points + j) as u32;
194                let c = (ring_b * num_profile_points + j_next) as u32;
195                let d = (ring_a * num_profile_points + j_next) as u32;
196                indices.push(a);
197                indices.push(b);
198                indices.push(c);
199                indices.push(a);
200                indices.push(c);
201                indices.push(d);
202            }
203        }
204
205        // End caps for a partial revolution.
206        //
207        // Originally a fan from the profile centroid to consecutive
208        // boundary points. That assumption only holds for CONVEX
209        // profiles — for a concave profile (I-beam, L-beam, hollow
210        // rectangle …) the centroid lies outside the polygon in some
211        // regions, the fan triangles cross each other, and the cap
212        // renders as a bow-tie/X artifact (issue #846 follow-up: PR
213        // #848 sweep landed correctly but the I-beam cross-section came
214        // out as a zigzag because of this fan path).
215        //
216        // Use earcut on the 2D profile boundary instead. The resulting
217        // triangle indices are in [0..num_profile_points) — they map
218        // 1:1 onto the ring vertices we already emitted, so the cap
219        // just reuses those positions (no new vertices except the side-
220        // wall winding requires flipping one of the two caps so its
221        // outward normal points away from the swept volume).
222        if !full_circle && num_profile_points >= 3 {
223            let profile_flat: Vec<f64> = profile_points
224                .iter()
225                .flat_map(|p| [p.x, p.y])
226                .collect();
227            let cap_indices = crate::triangulation::safe_earcut(&profile_flat, &[], 2)
228                .map_err(|e| Error::geometry(format!(
229                    "Revolved profile cap triangulation failed: {e}"
230                )))?;
231
232            for (ring_idx, flip) in [(0usize, true), (segments, false)] {
233                let base = (ring_idx * num_profile_points) as u32;
234                for tri in cap_indices.chunks_exact(3) {
235                    let a = base + tri[0] as u32;
236                    let b = base + tri[1] as u32;
237                    let c = base + tri[2] as u32;
238                    if flip {
239                        indices.push(a);
240                        indices.push(c);
241                        indices.push(b);
242                    } else {
243                        indices.push(a);
244                        indices.push(b);
245                        indices.push(c);
246                    }
247                }
248            }
249        }
250
251        let mut mesh = Mesh {
252            positions,
253            normals: Vec::new(),
254            indices,
255            rtc_applied: false, 
256            origin: [0.0; 3],        instance_meta: None, local_bounds: None, local_to_world: None };
257
258        // Apply Position to lift Position-local coords into object coords.
259        apply_transform(&mut mesh, &position_transform);
260
261        // Profile-boundary creases (e.g. flange-to-web on an I-beam) are
262        // all sharp 90° edges, but the swept mesh shares vertices between
263        // adjacent side quads — so per-vertex normal averaging smooths the
264        // shading across every crease and the cross-section reads as a
265        // smooth blob. Flat-shade the whole revolved solid (each triangle
266        // gets its own three vertices with the face normal) so the
267        // shading matches the actual geometry.
268        let flat =
269            PolygonalFaceSetProcessor::build_flat_shaded_mesh(&mesh.positions, &mesh.indices);
270        mesh.positions = flat.positions;
271        mesh.normals = flat.normals;
272        mesh.indices = flat.indices;
273
274        Ok(mesh)
275    }
276
277    fn supported_types(&self) -> Vec<IfcType> {
278        vec![IfcType::IfcRevolvedAreaSolid]
279    }
280}
281
282impl Default for RevolvedAreaSolidProcessor {
283    fn default() -> Self {
284        Self::new(IfcSchema::new())
285    }
286}