1use super::outline::{approximate_arc_3pt_3d, same_point_3d, trim_polyline};
6use super::ProfileProcessor;
7use crate::tessellation::scale_segments;
8use crate::{Error, Point3, Result, Vector3};
9use ifc_lite_core::{AttributeValue, DecodedEntity, EntityDecoder, IfcType};
10use std::f64::consts::PI;
11
12impl ProfileProcessor {
13 fn read_line_3d(
20 &self,
21 line: &DecodedEntity,
22 decoder: &mut EntityDecoder,
23 ) -> Result<(Point3<f64>, Vector3<f64>)> {
24 let (origin, direction) = crate::curve_source::mesh_line_basis(line, decoder)?;
25 Ok((Point3::from(origin), Vector3::from(direction)))
26 }
27
28 pub fn get_line_points_3d(
32 &self,
33 line: &DecodedEntity,
34 decoder: &mut EntityDecoder,
35 t_start: f64,
36 t_end: f64,
37 ) -> Result<Vec<Point3<f64>>> {
38 let (origin, v) = self.read_line_3d(line, decoder)?;
39 Ok(vec![origin + v * t_start, origin + v * t_end])
40 }
41
42 pub(super) fn process_trimmed_line_3d(
44 &self,
45 trimmed: &DecodedEntity,
46 line: &DecodedEntity,
47 decoder: &mut EntityDecoder,
48 ) -> Result<Vec<Point3<f64>>> {
49 use crate::trimmed_curve::{decode_trimmed_primitive, TrimRecovery, TrimmedPrimitive};
50 let Some(TrimmedPrimitive::Line { start, end }) =
51 decode_trimmed_primitive(trimmed, line, decoder, TrimRecovery::BasisDefaults)?
52 else {
53 return Err(Error::geometry("Expected trimmed line".to_string()));
54 };
55 Ok(vec![Point3::from(start), Point3::from(end)])
56 }
57
58 pub(super) fn read_conic_placement_3d(
65 &self,
66 curve: &DecodedEntity,
67 decoder: &mut EntityDecoder,
68 ) -> Result<(Point3<f64>, Vector3<f64>, Vector3<f64>)> {
69 let position_attr = curve
70 .get(0)
71 .ok_or_else(|| Error::geometry("Conic missing Position".to_string()))?;
72 let position = decoder
73 .resolve_ref(position_attr)?
74 .ok_or_else(|| Error::geometry("Failed to resolve conic position".to_string()))?;
75
76 if position.ifc_type == IfcType::IfcAxis2Placement3D {
77 let loc_attr = position
79 .get(0)
80 .ok_or_else(|| Error::geometry("Axis2Placement3D missing Location".to_string()))?;
81 let loc = decoder
82 .resolve_ref(loc_attr)?
83 .ok_or_else(|| Error::geometry("Failed to resolve location".to_string()))?;
84 let coords = loc
85 .get(0)
86 .and_then(|v| v.as_list())
87 .ok_or_else(|| Error::geometry("Location missing coordinates".to_string()))?;
88 let center = Point3::new(
89 coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
90 coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
91 coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
92 );
93
94 let z_axis = if let Some(axis_attr) = position.get(1) {
96 if !axis_attr.is_null() {
97 let axis = decoder.resolve_ref(axis_attr)?;
98 if let Some(axis) = axis {
99 let coords = axis.get(0).and_then(|v| v.as_list());
100 if let Some(coords) = coords {
101 Vector3::new(
102 coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
103 coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
104 coords.get(2).and_then(|v| v.as_float()).unwrap_or(1.0),
105 )
106 .normalize()
107 } else {
108 Vector3::new(0.0, 0.0, 1.0)
109 }
110 } else {
111 Vector3::new(0.0, 0.0, 1.0)
112 }
113 } else {
114 Vector3::new(0.0, 0.0, 1.0)
115 }
116 } else {
117 Vector3::new(0.0, 0.0, 1.0)
118 };
119
120 let x_axis = if let Some(ref_attr) = position.get(2) {
122 if !ref_attr.is_null() {
123 let ref_dir = decoder.resolve_ref(ref_attr)?;
124 if let Some(ref_dir) = ref_dir {
125 let coords = ref_dir.get(0).and_then(|v| v.as_list());
126 if let Some(coords) = coords {
127 Vector3::new(
128 coords.first().and_then(|v| v.as_float()).unwrap_or(1.0),
129 coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
130 coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
131 )
132 .normalize()
133 } else {
134 Vector3::new(1.0, 0.0, 0.0)
135 }
136 } else {
137 Vector3::new(1.0, 0.0, 0.0)
138 }
139 } else {
140 Vector3::new(1.0, 0.0, 0.0)
141 }
142 } else {
143 Vector3::new(1.0, 0.0, 0.0)
144 };
145
146 let y_axis = z_axis.cross(&x_axis).normalize();
148
149 Ok((center, x_axis, y_axis))
150 } else {
151 let loc_attr = position.get(0);
155 let (cx, cy) = if let Some(attr) = loc_attr {
156 let loc = decoder.resolve_ref(attr)?;
157 if let Some(loc) = loc {
158 let coords = loc.get(0).and_then(|v| v.as_list());
159 if let Some(coords) = coords {
160 (
161 coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
162 coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
163 )
164 } else {
165 (0.0, 0.0)
166 }
167 } else {
168 (0.0, 0.0)
169 }
170 } else {
171 (0.0, 0.0)
172 };
173 let x_axis = position
174 .get(1)
175 .filter(|a| !a.is_null())
176 .and_then(|a| decoder.resolve_ref(a).ok().flatten())
177 .and_then(|d| {
178 let coords = d.get(0).and_then(|v| v.as_list())?;
179 Some(Vector3::new(
180 coords.first().and_then(|v| v.as_float()).unwrap_or(1.0),
181 coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
182 0.0,
183 ))
184 })
185 .and_then(|v| v.try_normalize(1e-12))
186 .unwrap_or_else(|| Vector3::new(1.0, 0.0, 0.0));
187 let y_axis = Vector3::new(-x_axis.y, x_axis.x, 0.0);
189 Ok((Point3::new(cx, cy, 0.0), x_axis, y_axis))
190 }
191 }
192
193 pub(super) fn process_circle_3d(
195 &self,
196 curve: &DecodedEntity,
197 decoder: &mut EntityDecoder,
198 ) -> Result<Vec<Point3<f64>>> {
199 let radius = curve
201 .get_float(1)
202 .ok_or_else(|| Error::geometry("Circle missing Radius".to_string()))?;
203
204 let (center, x_axis, y_axis) = self.read_conic_placement_3d(curve, decoder)?;
205
206 let segments = scale_segments(24, 8, 96, self.quality());
208 let mut points = Vec::with_capacity(segments + 1);
209
210 for i in 0..=segments {
211 let angle = 2.0 * std::f64::consts::PI * i as f64 / segments as f64;
212 let p = center + x_axis * (radius * angle.cos()) + y_axis * (radius * angle.sin());
213 points.push(p);
214 }
215
216 Ok(points)
217 }
218
219 pub(super) fn process_trimmed_conic_3d(
229 &self,
230 trimmed: &DecodedEntity,
231 basis: &DecodedEntity,
232 decoder: &mut EntityDecoder,
233 ) -> Result<Vec<Point3<f64>>> {
234 if basis.ifc_type == IfcType::IfcCircle {
235 use crate::trimmed_curve::{decode_trimmed_primitive, TrimRecovery, TrimmedPrimitive};
236 let decoded = match decode_trimmed_primitive(
239 trimmed, basis, decoder, TrimRecovery::BasisDefaults,
240 ) {
241 Ok(decoded) => decoded,
242 Err(_) => return self.process_trimmed_ellipse_3d(trimmed, basis, decoder),
243 };
244 let Some(TrimmedPrimitive::Circle {
245 center, normal: _, x_axis, y_axis, radius, start_angle, sweep_angle,
246 }) = decoded
247 else {
248 return Err(Error::geometry("Expected trimmed circle".to_string()));
249 };
250 return Ok(self.sample_conic_arc_3d(
251 (Point3::from(center), Vector3::from(x_axis), Vector3::from(y_axis)),
252 (radius, radius), start_angle, start_angle + sweep_angle, decoder,
253 ));
254 }
255 self.process_trimmed_ellipse_3d(trimmed, basis, decoder)
256 }
257
258 fn process_trimmed_ellipse_3d(
261 &self,
262 trimmed: &DecodedEntity,
263 basis: &DecodedEntity,
264 decoder: &mut EntityDecoder,
265 ) -> Result<Vec<Point3<f64>>> {
266 let radius = basis.get_float(1).unwrap_or(1.0);
267 let radius2 = if basis.ifc_type == IfcType::IfcEllipse {
268 basis.get_float(2).unwrap_or(radius)
269 } else {
270 radius
271 };
272
273 let (center, x_axis, y_axis) = self.read_conic_placement_3d(basis, decoder)?;
274
275 let prefer_cartesian = trimmed
278 .get(4)
279 .and_then(|v| v.as_enum())
280 .map(|m| m == "CARTESIAN")
281 .unwrap_or(false);
282
283 let sense = trimmed
284 .get(3)
285 .and_then(|v| match v {
286 AttributeValue::Enum(s) => Some(s == "T"),
287 _ => None,
288 })
289 .unwrap_or(true);
290
291 let angle_scale = decoder.plane_angle_to_radians();
292 let start_angle = self
293 .trim_to_angle_3d(
294 trimmed.get(1),
295 prefer_cartesian,
296 ¢er,
297 &x_axis,
298 &y_axis,
299 radius,
300 radius2,
301 angle_scale,
302 decoder,
303 )
304 .unwrap_or(0.0);
305 let mut end_angle = self
306 .trim_to_angle_3d(
307 trimmed.get(2),
308 prefer_cartesian,
309 ¢er,
310 &x_axis,
311 &y_axis,
312 radius,
313 radius2,
314 angle_scale,
315 decoder,
316 )
317 .unwrap_or(2.0 * PI);
318
319 if sense && end_angle < start_angle {
322 end_angle += 2.0 * PI;
323 } else if !sense && end_angle > start_angle {
324 end_angle -= 2.0 * PI;
325 }
326
327 Ok(self.sample_conic_arc_3d(
328 (center, x_axis, y_axis),
329 (radius, radius2),
330 start_angle,
331 end_angle,
332 decoder,
333 ))
334 }
335
336 #[allow(clippy::too_many_arguments)]
343 fn trim_to_angle_3d(
344 &self,
345 attr: Option<&AttributeValue>,
346 prefer_cartesian: bool,
347 center: &Point3<f64>,
348 x_axis: &Vector3<f64>,
349 y_axis: &Vector3<f64>,
350 radius: f64,
351 radius2: f64,
352 angle_scale: f64,
353 decoder: &mut EntityDecoder,
354 ) -> Option<f64> {
355 let list = attr?.as_list()?;
356 let mut param: Option<f64> = None;
357 let mut point: Option<Point3<f64>> = None;
358
359 for item in list {
360 if let Some(inner_list) = item.as_list() {
362 if let Some(type_name) = inner_list.first().and_then(|v| v.as_string()) {
363 if type_name == "IFCPARAMETERVALUE" {
364 param = inner_list.get(1).and_then(|v| v.as_float());
365 continue;
366 }
367 }
368 }
369 if item.as_entity_ref().is_some() {
371 if let Ok(Some(pt)) = decoder.resolve_ref(item) {
372 if pt.ifc_type == IfcType::IfcCartesianPoint {
373 if let Some(coords) = pt.get(0).and_then(|v| v.as_list()) {
374 point = Some(Point3::new(
375 coords.first().and_then(|v| v.as_float()).unwrap_or(0.0),
376 coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0),
377 coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0),
378 ));
379 }
380 }
381 }
382 continue;
383 }
384 if let Some(f) = item.as_float() {
386 param = Some(f);
387 }
388 }
389
390 let use_point = (prefer_cartesian && point.is_some()) || param.is_none();
391 if use_point {
392 if let Some(p) = point {
393 let d = p - center;
394 let lx = d.dot(x_axis);
395 let ly = d.dot(y_axis);
396 return Some((ly / radius2).atan2(lx / radius));
397 }
398 }
399 param.map(|v| v * angle_scale)
400 }
401
402 pub(super) fn process_polyline_3d(
404 &self,
405 curve: &DecodedEntity,
406 decoder: &mut EntityDecoder,
407 ) -> Result<Vec<Point3<f64>>> {
408 let points_attr = curve
410 .get(0)
411 .ok_or_else(|| Error::geometry("Polyline missing Points".to_string()))?;
412
413 let points = decoder.resolve_ref_list(points_attr)?;
414 let mut result = Vec::with_capacity(points.len());
415
416 for point in points {
417 let coords_attr = point
419 .get(0)
420 .ok_or_else(|| Error::geometry("CartesianPoint missing Coordinates".to_string()))?;
421
422 let coords = coords_attr
423 .as_list()
424 .ok_or_else(|| Error::geometry("Coordinates is not a list".to_string()))?;
425
426 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
427 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
428 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
429
430 result.push(Point3::new(x, y, z));
431 }
432
433 Ok(result)
434 }
435
436 pub fn get_polyline_points_trimmed(
443 &self,
444 curve: &DecodedEntity,
445 decoder: &mut EntityDecoder,
446 start_param: Option<f64>,
447 end_param: Option<f64>,
448 ) -> Result<Vec<Point3<f64>>> {
449 let points = self.process_polyline_3d(curve, decoder)?;
450 if points.len() < 2 {
451 return Ok(points);
452 }
453 let max_param = (points.len() - 1) as f64;
454 let s = start_param.unwrap_or(0.0).clamp(0.0, max_param);
455 let e = end_param.unwrap_or(max_param).clamp(0.0, max_param);
456 if e <= s {
457 return Ok(Vec::new());
458 }
459 Ok(trim_polyline(&points, s / max_param, e / max_param))
461 }
462
463 pub(super) fn process_indexed_polycurve_3d(
474 &self,
475 curve: &DecodedEntity,
476 decoder: &mut EntityDecoder,
477 ) -> Result<Vec<Point3<f64>>> {
478 let points_attr = curve
479 .get(0)
480 .ok_or_else(|| Error::geometry("IndexedPolyCurve missing Points".to_string()))?;
481
482 let points_list = decoder
483 .resolve_ref(points_attr)?
484 .ok_or_else(|| Error::geometry("Failed to resolve Points list".to_string()))?;
485
486 let coord_list = points_list
487 .get(0)
488 .and_then(|a| a.as_list())
489 .ok_or_else(|| Error::geometry("CartesianPointList missing CoordList".to_string()))?;
490
491 let all_points: Vec<Point3<f64>> = coord_list
494 .iter()
495 .filter_map(|coord| {
496 coord.as_list().map(|coords| {
497 let x = coords.first().and_then(|v| v.as_float()).unwrap_or(0.0);
498 let y = coords.get(1).and_then(|v| v.as_float()).unwrap_or(0.0);
499 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
500 Point3::new(x, y, z)
501 })
502 })
503 .collect();
504
505 let segments_attr = curve.get(1);
506 if segments_attr.is_none() || segments_attr.map(|a| a.is_null()).unwrap_or(true) {
507 return Ok(all_points);
508 }
509
510 let segments = segments_attr
511 .unwrap()
512 .as_list()
513 .ok_or_else(|| Error::geometry("Expected segments list".to_string()))?;
514
515 let mut result: Vec<Point3<f64>> = Vec::new();
516 for segment in segments {
517 let (is_arc, indices) = if let Some(segment_list) = segment.as_list() {
520 if segment_list.len() >= 2 {
521 let type_name = segment_list
522 .first()
523 .and_then(|v| v.as_string())
524 .unwrap_or("");
525 let is_arc_type = type_name.to_uppercase().contains("ARC");
526 if let Some(AttributeValue::List(indices_list)) = segment_list.get(1) {
527 (is_arc_type, Some(indices_list.as_slice()))
528 } else {
529 (false, Some(segment_list))
530 }
531 } else {
532 (false, Some(segment_list))
533 }
534 } else {
535 (false, None)
536 };
537
538 let Some(indices) = indices else { continue };
539 let idx_values: Vec<usize> = indices
540 .iter()
541 .filter_map(|v| v.as_float())
542 .filter_map(|f| {
545 if !f.is_finite() || f < 1.0 || f.fract() != 0.0 {
546 return None;
547 }
548 (f as usize).checked_sub(1)
549 })
550 .collect();
551
552 if is_arc && idx_values.len() == 3 {
553 let p1 = all_points.get(idx_values[0]).copied();
554 let p2 = all_points.get(idx_values[1]).copied();
555 let p3 = all_points.get(idx_values[2]).copied();
556 if let (Some(start), Some(mid), Some(end)) = (p1, p2, p3) {
557 let chord = end - start;
560 let chord_len = chord.norm();
561 let mid_offset = mid - Point3::new(
562 0.5 * (start.x + end.x),
563 0.5 * (start.y + end.y),
564 0.5 * (start.z + end.z),
565 );
566 let mid_dev = mid_offset.norm();
567 let arc_estimate = if chord_len > 1e-10 {
570 4.0 * (2.0 * mid_dev / chord_len).atan()
571 } else {
572 0.5
573 };
574 let arc_base =
575 (arc_estimate / std::f64::consts::FRAC_PI_2 * 8.0).ceil() as usize;
576 let num_segments = scale_segments(arc_base, 4, 16, self.quality());
577 let arc_points = approximate_arc_3pt_3d(start, mid, end, num_segments);
578 for pt in arc_points {
579 if !same_point_3d(result.last(), &pt) {
580 result.push(pt);
581 }
582 }
583 }
584 } else {
585 for &idx in &idx_values {
587 if let Some(&pt) = all_points.get(idx) {
588 if !same_point_3d(result.last(), &pt) {
589 result.push(pt);
590 }
591 }
592 }
593 }
594 }
595
596 Ok(result)
597 }
598}