ifc_lite_processing/scan_proposals/
mod.rs1mod cylinders;
23mod frame;
24mod levels;
25mod options;
26mod report;
27mod score;
28mod slabs;
29mod walls;
30
31pub use options::{ProposalSchema, ScanProposalOptions};
32pub use report::{
33 DetectionKind, DetectionRef, ProposalBasis, ProposalClass, ProposalFit, ProposalGeometry,
34 ScanElementProposal, ScanProposalReport, ScanProposalStats,
35};
36
37use crate::scan_segmentation::ScanSegmentationReport;
38use frame::{Face, Similarity, V3};
39use score::Draft;
40
41const ALGORITHM: &str = "ifclite-scan-proposals-v1";
42pub const MAX_PLANES: usize = 20_000;
44pub const MAX_CYLINDERS: usize = 20_000;
45
46pub fn propose_scan_elements(
48 report: &ScanSegmentationReport,
49 options: &ScanProposalOptions,
50) -> Result<ScanProposalReport, String> {
51 let params = options.validate()?;
52 if report.planes.len() > MAX_PLANES || report.cylinders.len() > MAX_CYLINDERS {
53 return Err(format!("Scan proposals accept at most {MAX_PLANES} planes and {MAX_CYLINDERS} cylinders"));
54 }
55 let transform = Similarity::from_row_major(&options.scan_to_model)?;
56 let detections = frame::to_model(report, &transform);
57 let faces = detections.faces;
58 let mut stats = ScanProposalStats::default();
59 let (mut vertical, mut horizontal) = (Vec::new(), Vec::new());
60 for (i, face) in faces.iter().enumerate() {
61 let up = face.normal[2].abs();
62 if up >= params.cos_class {
63 horizontal.push(i);
64 } else if up <= params.sin_class {
65 vertical.push(i);
66 } else {
67 stats.sloped_planes += 1;
68 }
69 }
70 stats.vertical_planes = vertical.len() as u32;
71 stats.horizontal_planes = horizontal.len() as u32;
72 let interior = params.interior.unwrap_or_else(|| area_weighted_centroid(&faces));
73 let levels = levels::classify(&faces, &horizontal, &vertical, interior, params.snap);
74
75 let walls = walls::propose(&faces, &vertical, &levels, interior, ¶ms, &mut stats);
76 let slabs = slabs::propose(&faces, &levels, ¶ms, &mut stats);
77 let tubes = cylinders::propose(&detections.tubes, &faces, &levels, ¶ms, &mut stats);
78 let mut proposals = Vec::new();
79 number("wall", walls, &mut proposals);
80 number("slab", slabs, &mut proposals);
81 let (columns, pipes): (Vec<Draft>, Vec<Draft>) =
82 tubes.into_iter().partition(|d| d.class == ProposalClass::IfcColumn);
83 number("column", columns, &mut proposals);
84 number("pipe", pipes, &mut proposals);
85 Ok(ScanProposalReport { algorithm: ALGORITHM.into(), proposals, transform_scale: transform.scale, stats })
86}
87
88fn number(prefix: &str, mut drafts: Vec<Draft>, out: &mut Vec<ScanElementProposal>) {
90 drafts.sort_by(|a, b| {
91 b.support.total_cmp(&a.support).then_with(|| a.sources.first().map(|s| s.index).cmp(&b.sources.first().map(|s| s.index)))
92 });
93 for (i, d) in drafts.into_iter().enumerate() {
94 out.push(ScanElementProposal {
95 id: format!("{prefix}-{i}"),
96 ifc_class: d.class,
97 confidence: d.confidence,
98 basis: d.basis,
99 sources: d.sources,
100 fit: d.fit,
101 geometry: d.geometry,
102 });
103 }
104}
105
106fn area_weighted_centroid(faces: &[Face]) -> V3 {
107 let total: f64 = faces.iter().map(|f| f.area).sum();
108 if total <= 0. {
109 return [0.; 3];
110 }
111 let mut sum = [0.; 3];
112 for f in faces {
113 for (s, c) in sum.iter_mut().zip(f.centroid) {
114 *s += c * f.area / total;
115 }
116 }
117 sum
118}
119
120#[cfg(test)]
121#[path = "proposals_tests.rs"]
122mod tests;