ifc_lite_processing/analytic_export/
metrics.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//! Measurements derived from complete world-space analytic directrices.
6
7use ifc_lite_geometry::analytic::AnalyticCurveSegment;
8use serde::Serialize;
9
10/// Per-segment measurements in metres and radians, in directrix order.
11#[derive(Debug, Clone, Serialize)]
12#[non_exhaustive]
13pub struct DirectrixSegmentMetrics {
14    /// Index of the corresponding entry in `Directrix`.
15    pub segment_index: usize,
16    /// Centreline length in metres.
17    pub length: f64,
18    /// Arc sweep magnitude in radians; absent for a line. The directrix keeps
19    /// the signed sweep angle for callers that need traversal orientation.
20    pub bend_angle: Option<f64>,
21}
22
23/// Measurements of a complete occurrence directrix in absolute IFC world space.
24#[derive(Debug, Clone, Serialize)]
25#[non_exhaustive]
26pub struct DirectrixMetrics {
27    /// Sum of the ordered segment lengths, in metres.
28    pub total_length: f64,
29    pub segments: Vec<DirectrixSegmentMetrics>,
30}
31
32impl DirectrixMetrics {
33    pub(super) fn from_segments(segments: &[AnalyticCurveSegment]) -> Result<Self, &'static str> {
34        let mut result = Self {
35            total_length: 0.0,
36            segments: Vec::with_capacity(segments.len()),
37        };
38        for (segment_index, segment) in segments.iter().enumerate() {
39            let (length, bend_angle) = match segment {
40                AnalyticCurveSegment::Line { .. } =>
41                    (segment.length().ok_or("directrix measurements are not finite")?, None),
42                AnalyticCurveSegment::Arc { radius, sweep_angle, .. } => {
43                    if *radius <= 0.0 {
44                        return Err("directrix arc radius is not positive");
45                    }
46                    (segment.length().ok_or("directrix measurements are not finite")?, Some(sweep_angle.abs()))
47                }
48            };
49            if !length.is_finite() || bend_angle.is_some_and(|angle| !angle.is_finite()) {
50                return Err("directrix measurements are not finite");
51            }
52            result.total_length += length;
53            if !result.total_length.is_finite() {
54                return Err("directrix total length is not finite");
55            }
56            result.segments.push(DirectrixSegmentMetrics { segment_index, length, bend_angle });
57        }
58        Ok(result)
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn negative_arc_sweep_reports_positive_bend_and_length() {
68        let arc = AnalyticCurveSegment::Arc {
69            center: [0.0; 3],
70            normal: [0.0, 0.0, 1.0],
71            x_axis: [1.0, 0.0, 0.0],
72            radius: 2.0,
73            start_angle: 0.0,
74            sweep_angle: -std::f64::consts::PI,
75        };
76        let metrics = DirectrixMetrics::from_segments(&[arc]).unwrap();
77        assert_eq!(metrics.total_length, 2.0 * std::f64::consts::PI);
78        assert_eq!(metrics.segments[0].bend_angle, Some(std::f64::consts::PI));
79    }
80
81    #[test]
82    fn overflowing_line_or_total_has_no_partial_result() {
83        let line = |start, end| AnalyticCurveSegment::Line {
84            start: [start, 0.0, 0.0],
85            end: [end, 0.0, 0.0],
86        };
87        assert!(DirectrixMetrics::from_segments(&[line(-1e308, 1e308)]).is_err());
88        assert!(DirectrixMetrics::from_segments(&[line(0.0, 1e308), line(0.0, 1e308)]).is_err());
89    }
90}