1use ifc_lite_geometry::analytic::{AnalyticCurveSegment, AnalyticStatus};
8use ifc_lite_processing::{
9 DirectrixMetrics, SweptDiskCheckError, SweptDiskCheckReport, SweptDiskFindingCode,
10 SweptDiskOccurrence,
11};
12use serde::Serialize;
13
14#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
16#[non_exhaustive]
17pub struct RebarPreflightLimits {
18 pub min_inside_bend_radius_m: f64,
19 pub min_straight_segment_length_m: f64,
20 pub max_developed_centreline_length_m: Option<f64>,
21}
22
23impl RebarPreflightLimits {
24 pub fn new(min_inside_bend_radius_m: f64, min_straight_segment_length_m: f64,
25 max_developed_centreline_length_m: Option<f64>) -> Result<Self, RebarPreflightError> {
26 let limits = Self { min_inside_bend_radius_m, min_straight_segment_length_m,
27 max_developed_centreline_length_m };
28 limits.validate()?;
29 Ok(limits)
30 }
31
32 pub fn validate(&self) -> Result<(), RebarPreflightError> {
33 for (name, value) in [
34 ("min_inside_bend_radius_m", self.min_inside_bend_radius_m),
35 ("min_straight_segment_length_m", self.min_straight_segment_length_m),
36 ] {
37 if !value.is_finite() || value < 0.0 {
38 return Err(RebarPreflightError { option: name });
39 }
40 }
41 if self.max_developed_centreline_length_m.is_some_and(|value| !value.is_finite() || value < 0.0) {
42 return Err(RebarPreflightError { option: "max_developed_centreline_length_m" });
43 }
44 Ok(())
45 }
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct RebarPreflightError { pub option: &'static str }
51
52impl std::fmt::Display for RebarPreflightError {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 write!(f, "{} must be finite and nonnegative", self.option)
55 }
56}
57
58impl std::error::Error for RebarPreflightError {}
59
60#[derive(Debug)]
62pub enum RebarSchedulePreflightError {
63 Limits(RebarPreflightError),
64 SweepChecks(SweptDiskCheckError),
65}
66
67impl std::fmt::Display for RebarSchedulePreflightError {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 Self::Limits(error) => error.fmt(f),
71 Self::SweepChecks(error) => error.fmt(f),
72 }
73 }
74}
75
76impl std::error::Error for RebarSchedulePreflightError {}
77
78#[derive(Debug, Clone, PartialEq, Serialize)]
80#[non_exhaustive]
81pub struct RebarPreflightComparison {
82 pub kind: &'static str,
83 pub segment_index: Option<usize>,
84 pub measured_m: f64,
85 pub limit_m: f64,
86 pub passed: bool,
87}
88
89#[derive(Debug, Clone, PartialEq, Serialize)]
91#[non_exhaustive]
92pub struct RebarPreflightReport {
93 pub skipped_reason: Option<String>,
94 pub comparisons: Option<Vec<RebarPreflightComparison>>,
96 pub unassessed_reasons: Vec<String>,
98}
99
100pub(super) fn assess_sweep(
101 disk: &SweptDiskOccurrence,
102 checks: &SweptDiskCheckReport,
103 metrics: Option<&DirectrixMetrics>,
104 limits: &RebarPreflightLimits,
105) -> RebarPreflightReport {
106 let mut report = RebarPreflightReport { skipped_reason: None, comparisons: None, unassessed_reasons: Vec::new() };
107 if disk.source_modified {
108 report.skipped_reason = Some("source is modified by enclosing CSG; finished bar geometry is unavailable".into());
109 return report;
110 }
111 if let AnalyticStatus::Unsupported(reason) = &disk.status {
112 report.skipped_reason = Some(format!("unsupported directrix: {reason}"));
113 return report;
114 }
115 if let Some(reason) = &checks.skipped_reason {
116 report.skipped_reason = Some(format!("source geometry checks could not run: {reason}"));
117 return report;
118 }
119 if let Some(finding) = checks.findings.iter().find(|finding| matches!(
120 finding.code,
121 SweptDiskFindingCode::ConsecutiveGap | SweptDiskFindingCode::ZeroLengthSegment
122 )) {
123 let defect = match finding.code {
124 SweptDiskFindingCode::ConsecutiveGap => "a consecutive gap",
125 SweptDiskFindingCode::ZeroLengthSegment => "a zero-length segment",
126 _ => unreachable!(),
127 };
128 report.skipped_reason = Some(format!(
129 "source directrix has {defect} at segment {}; whole-sweep comparisons are unavailable",
130 finding.segment_index,
131 ));
132 return report;
133 }
134 let Some(metrics) = metrics else {
135 report.skipped_reason = Some("directrix measurements are unavailable".into());
136 return report;
137 };
138 if disk.directrix.is_empty() || !disk.radius.is_finite() || disk.radius <= 0.0 {
139 report.skipped_reason = Some("directrix or swept outer radius is unavailable".into());
140 return report;
141 }
142 if disk.directrix.len() != metrics.segments.len() {
143 report.skipped_reason = Some("directrix segment measurements are incomplete".into());
144 return report;
145 }
146 let mut has_arc = false;
147 let mut has_line = false;
148 let mut comparisons = Vec::with_capacity(
149 disk.directrix.len() + usize::from(limits.max_developed_centreline_length_m.is_some()),
150 );
151 for (segment, metric) in disk.directrix.iter().zip(metrics.segments.iter()) {
152 let (kind, measured_m, limit_m, passed) = match segment {
153 AnalyticCurveSegment::Line { .. } => { has_line = true; (
154 "straight_segment_length",
155 metric.length,
156 limits.min_straight_segment_length_m,
157 metric.length >= limits.min_straight_segment_length_m,
158 ) },
159 AnalyticCurveSegment::Arc { radius, .. } => {
160 has_arc = true;
161 let inside_radius = radius - disk.radius;
162 (
163 "inside_bend_radius",
164 inside_radius,
165 limits.min_inside_bend_radius_m,
166 inside_radius >= limits.min_inside_bend_radius_m,
167 )
168 }
169 };
170 comparisons.push(RebarPreflightComparison {
171 kind, segment_index: Some(metric.segment_index), measured_m, limit_m, passed,
172 });
173 }
174 if !has_arc {
175 report.unassessed_reasons.push("inside bend radius: no arc segments".into());
176 }
177 if !has_line {
178 report.unassessed_reasons.push("straight segment length: no line segments".into());
179 }
180 if let Some(limit_m) = limits.max_developed_centreline_length_m {
181 comparisons.push(RebarPreflightComparison {
182 kind: "developed_centreline_length", segment_index: None,
183 measured_m: metrics.total_length, limit_m,
184 passed: metrics.total_length <= limit_m,
185 });
186 }
187 report.comparisons = Some(comparisons);
188 report
189}