1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! SurfaceOfLinearExtrusion processor - surface sweep geometry.
use crate::{Error, Mesh, Point2, Point3, Result, TessellationQuality, Vector3};
use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
use nalgebra::Matrix4;
use super::helpers::{get_axis2_placement_transform_by_id, get_direction_by_id};
use crate::router::GeometryProcessor;
/// SurfaceOfLinearExtrusion processor
/// Handles IfcSurfaceOfLinearExtrusion - surface created by sweeping a curve along a direction
pub struct SurfaceOfLinearExtrusionProcessor;
#[path = "curve_walk.rs"]
mod curve_walk;
use curve_walk::{CurveWalk, MAX_CURVE_NODES, SEAM_EPS};
impl SurfaceOfLinearExtrusionProcessor {
pub fn new() -> Self {
Self
}
}
impl GeometryProcessor for SurfaceOfLinearExtrusionProcessor {
fn process(
&self,
entity: &DecodedEntity,
decoder: &mut EntityDecoder,
_schema: &IfcSchema,
_quality: TessellationQuality,
) -> Result<Mesh> {
// IfcSurfaceOfLinearExtrusion attributes:
// 0: SweptCurve (IfcProfileDef - usually IfcArbitraryOpenProfileDef)
// 1: Position (IfcAxis2Placement3D)
// 2: ExtrudedDirection (IfcDirection)
// 3: Depth (length)
// Get the swept curve (profile)
let curve_attr = entity.get(0).ok_or_else(|| {
Error::geometry("SurfaceOfLinearExtrusion missing SweptCurve".to_string())
})?;
let curve_id = curve_attr.as_entity_ref().ok_or_else(|| {
Error::geometry("Expected entity reference for SweptCurve".to_string())
})?;
// Get position
let position_attr = entity.get(1);
let position_transform = if let Some(attr) = position_attr {
if let Some(pos_id) = attr.as_entity_ref() {
get_axis2_placement_transform_by_id(pos_id, decoder)?
} else {
Matrix4::identity()
}
} else {
Matrix4::identity()
};
// Get extrusion direction
let direction_attr = entity.get(2).ok_or_else(|| {
Error::geometry("SurfaceOfLinearExtrusion missing ExtrudedDirection".to_string())
})?;
let direction = if let Some(dir_id) = direction_attr.as_entity_ref() {
get_direction_by_id(dir_id, decoder)
.ok_or_else(|| Error::geometry("Failed to get direction".to_string()))?
} else {
Vector3::new(0.0, 0.0, 1.0) // Default to Z-up
};
// Get depth
let depth = entity
.get(3)
.and_then(|v| v.as_float())
.ok_or_else(|| Error::geometry("SurfaceOfLinearExtrusion missing Depth".to_string()))?;
// Get curve points from the profile
let curve_points = Self::get_profile_curve_points(curve_id, decoder)?;
if curve_points.len() < 2 {
return Ok(Mesh::new());
}
// Extrude the curve to create a surface (quad strip)
let extrusion = direction.normalize() * depth;
let mut positions = Vec::with_capacity(curve_points.len() * 2 * 3);
let mut indices = Vec::with_capacity((curve_points.len() - 1) * 6);
// Create vertices: bottom row, then top row
for point in &curve_points {
// Transform 2D point to 3D using position
let p3d = position_transform.transform_point(&Point3::new(point.x, point.y, 0.0));
positions.push(p3d.x as f32);
positions.push(p3d.y as f32);
positions.push(p3d.z as f32);
}
for point in &curve_points {
// Extruded point
let p3d = position_transform.transform_point(&Point3::new(point.x, point.y, 0.0));
let p_extruded = p3d + extrusion;
positions.push(p_extruded.x as f32);
positions.push(p_extruded.y as f32);
positions.push(p_extruded.z as f32);
}
// Create quad strip triangles
let n = curve_points.len() as u32;
for i in 0..n - 1 {
// Two triangles per quad
// Triangle 1: bottom-left, bottom-right, top-left
indices.push(i);
indices.push(i + 1);
indices.push(i + n);
// Triangle 2: bottom-right, top-right, top-left
indices.push(i + 1);
indices.push(i + n + 1);
indices.push(i + n);
}
Ok(Mesh {
positions,
normals: Vec::new(),
indices,
rtc_applied: false,
origin: [0.0; 3], instance_meta: None, local_bounds: None, local_to_world: None })
}
fn supported_types(&self) -> Vec<IfcType> {
vec![IfcType::IfcSurfaceOfLinearExtrusion]
}
}
impl SurfaceOfLinearExtrusionProcessor {
/// Extract curve points from a profile definition
/// Longest nested-curve chain the profile sampler will follow. See
/// `curve_points_guarded` for why this sits alongside the visited set.
const MAX_CURVE_NESTING_DEPTH: u32 = 32;
fn get_profile_curve_points(
profile_id: u32,
decoder: &mut EntityDecoder,
) -> Result<Vec<Point2<f64>>> {
let mut walk = CurveWalk::new();
Self::profile_curve_points_guarded(profile_id, decoder, &mut walk)
}
fn profile_curve_points_guarded(
profile_id: u32,
decoder: &mut EntityDecoder,
walk: &mut CurveWalk,
) -> Result<Vec<Point2<f64>>> {
let profile = decoder.decode_by_id(profile_id)?;
// IfcArbitraryOpenProfileDef: 0=ProfileType, 1=ProfileName, 2=Curve
// IfcArbitraryClosedProfileDef: 0=ProfileType, 1=ProfileName, 2=OuterCurve
let curve_attr = profile
.get(2)
.ok_or_else(|| Error::geometry("Profile missing curve".to_string()))?;
let curve_id = curve_attr
.as_entity_ref()
.ok_or_else(|| Error::geometry("Expected entity reference for curve".to_string()))?;
Self::curve_points_guarded(curve_id, decoder, 0, walk)
}
/// Sample a CURVE (not a profile) into 2D points.
///
/// Split out of `get_profile_curve_points` because
/// `extract_composite_curve_points` was calling that function with a
/// segment's `ParentCurve` id — a curve where a profile was expected. It
/// read attribute 2 of the curve as "the profile's curve", and an
/// `IfcPolyline` has no attribute 2, so every composite-curve profile
/// errored on each segment, had the error swallowed by the caller's
/// `if let Ok(..)`, and returned `Ok(vec![])`. Silently: no points, no
/// error, indistinguishable from a legitimately empty profile.
///
/// Guarded by BOTH a visited set and a depth cap, because they bound
/// different things. The set stops cycles and fan-out --
/// `extract_composite_curve_points` loops over segments, so `k` segments
/// each leading back cost `O(k^depth)` and a cap alone would trade the
/// abort for a hang. The cap stops a long ACYCLIC chain, where every
/// insert succeeds, the set never fires, and the recursion aborts on stack
/// depth alone (Codex, #2871/#2872 review). Neither substitutes for the
/// other (#2866).
fn curve_points_guarded(
curve_id: u32,
decoder: &mut EntityDecoder,
depth: u32,
walk: &mut CurveWalk,
) -> Result<Vec<Point2<f64>>> {
if depth >= Self::MAX_CURVE_NESTING_DEPTH || !walk.seen.insert(curve_id) {
return Ok(Vec::new());
}
walk.spend()?;
let out = Self::curve_points_inner(curve_id, decoder, depth, walk);
// PATH-scoped: removed on the way out. A global set would be a memo
// that returns the WRONG value -- it hands back an empty vec rather
// than the points it computed the first time -- and the caller
// ACCUMULATES, so a ParentCurve legitimately reused by two segments
// would contribute once and silently shorten the profile.
walk.seen.remove(&curve_id);
out
}
fn curve_points_inner(
curve_id: u32,
decoder: &mut EntityDecoder,
depth: u32,
walk: &mut CurveWalk,
) -> Result<Vec<Point2<f64>>> {
// Get curve entity to determine type
let curve = decoder.decode_by_id(curve_id)?;
match curve.ifc_type {
IfcType::IfcPolyline => {
// IfcPolyline: attribute 0 is Points (list of IfcCartesianPoint)
let point_ids = decoder
.get_polyloop_point_ids_fast(curve_id)
.ok_or_else(|| Error::geometry("Failed to get polyline points".to_string()))?;
let mut points = Vec::with_capacity(point_ids.len());
for point_id in point_ids {
if let Some((x, y, _z)) = decoder.get_cartesian_point_fast(point_id) {
points.push(Point2::new(x, y));
}
}
Ok(points)
}
IfcType::IfcCompositeCurve => {
// Handle composite curves by extracting segments
Self::extract_composite_curve_points(curve_id, decoder, depth, walk)
}
_ => {
// Fallback: try to get points directly
if let Some(point_ids) = decoder.get_polyloop_point_ids_fast(curve_id) {
let mut points = Vec::with_capacity(point_ids.len());
for point_id in point_ids {
if let Some((x, y, _z)) = decoder.get_cartesian_point_fast(point_id) {
points.push(Point2::new(x, y));
}
}
Ok(points)
} else {
Ok(Vec::new())
}
}
}
}
/// Extract points from a composite curve
fn extract_composite_curve_points(
curve_id: u32,
decoder: &mut EntityDecoder,
depth: u32,
walk: &mut CurveWalk,
) -> Result<Vec<Point2<f64>>> {
let curve = decoder.decode_by_id(curve_id)?;
// IfcCompositeCurve: attribute 0 is Segments (list of IfcCompositeCurveSegment)
let segments_attr = curve
.get(0)
.ok_or_else(|| Error::geometry("CompositeCurve missing Segments".to_string()))?;
let segment_refs = segments_attr
.as_list()
.ok_or_else(|| Error::geometry("Expected segment list".to_string()))?;
let mut all_points: Vec<Point2<f64>> = Vec::new();
for seg_ref in segment_refs {
let seg_id = seg_ref.as_entity_ref().ok_or_else(|| {
Error::geometry("Expected entity reference for segment".to_string())
})?;
let segment = decoder.decode_by_id(seg_id)?;
// IfcCompositeCurveSegment: 0=Transition, 1=SameSense, 2=ParentCurve
let parent_curve_attr = segment
.get(2)
.ok_or_else(|| Error::geometry("Segment missing ParentCurve".to_string()))?;
let parent_curve_id = parent_curve_attr.as_entity_ref().ok_or_else(|| {
Error::geometry("Expected entity reference for parent curve".to_string())
})?;
// IfcCompositeCurveSegment.SameSense (attribute 1): when false the
// segment traverses its ParentCurve BACKWARDS. Nothing applied it
// before because no segment ever produced points to orient -- the
// dispatch bug above meant every one came back empty, so a
// reversed segment and a forward one were indistinguishable.
let same_sense = segment
.get(1)
.map(|v| match v {
ifc_lite_core::AttributeValue::Enum(e) => e != "F" && e != ".F.",
_ => true,
})
.unwrap_or(true);
// The ParentCurve is a CURVE, not a profile. Routing it through the
// profile entry point read its attribute 2 as "the curve" and
// dropped every segment (#2866).
if let Ok(mut segment_points) =
Self::curve_points_guarded(parent_curve_id, decoder, depth + 1, walk)
{
if !same_sense {
segment_points.reverse();
}
// Drop the seam point only when it ACTUALLY duplicates the
// previous segment's end. A `.DISCONTINUOUS.` transition, or a
// gap from a malformed file, leaves a real point that an
// unconditional skip would eat.
let drop_seam = match (all_points.last(), segment_points.first()) {
(Some(prev), Some(next)) => {
(prev.x - next.x).abs() < SEAM_EPS && (prev.y - next.y).abs() < SEAM_EPS
}
_ => false,
};
let start_idx = usize::from(drop_seam);
all_points.extend(segment_points.into_iter().skip(start_idx));
}
// The `if let Ok(..)` above deliberately tolerates ONE malformed
// segment rather than losing the whole profile -- but it must not
// swallow budget exhaustion, or the loop keeps going and returns a
// truncated profile as if it were complete. That is the silent
// wrong answer this guard exists to avoid, so exhaustion is
// re-raised here where the tolerance cannot hide it.
if walk.exhausted {
return Err(Error::geometry(format!(
"Curve traversal exceeded {MAX_CURVE_NODES} nested curves"
)));
}
}
Ok(all_points)
}
}
impl Default for SurfaceOfLinearExtrusionProcessor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[path = "surface_cycle_tests.rs"]
mod surface_cycle_tests;