ifc_lite_wasm/api/
scan_proposals.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/.
4use super::IfcAPI;
5use ifc_lite_processing::scan_proposals::{propose_scan_elements, ScanProposalOptions};
6use ifc_lite_processing::scan_segmentation::ScanSegmentationReport;
7use wasm_bindgen::prelude::*;
8
9const MAX_OPTIONS_BYTES: usize = 64 * 1024;
10/// A report of 20,000 planes and 20,000 cylinders is about 25 MiB of JSON.
11const MAX_REPORT_BYTES: usize = 64 * 1024 * 1024;
12
13fn propose_json(report_json: &str, options_json: &str) -> Result<Vec<u8>, String> {
14    if options_json.len() > MAX_OPTIONS_BYTES {
15        return Err("Scan proposal options exceed 64 KiB".into());
16    }
17    if report_json.len() > MAX_REPORT_BYTES {
18        return Err("Scan segmentation report exceeds 64 MiB".into());
19    }
20    let options: ScanProposalOptions = serde_json::from_str(options_json)
21        .map_err(|error| format!("Invalid scan proposal options: {error}"))?;
22    let report: ScanSegmentationReport = serde_json::from_str(report_json)
23        .map_err(|error| format!("Invalid scan segmentation report: {error}"))?;
24    let proposals = propose_scan_elements(&report, &options)?;
25    serde_json::to_vec(&proposals).map_err(|error| format!("Cannot encode scan proposals: {error}"))
26}
27
28#[wasm_bindgen]
29impl IfcAPI {
30    /// Propose IFC walls, slabs, columns and pipes from a scan segmentation
31    /// report (#6894). `report_json` is the JSON `segmentScanPoints`
32    /// returned; `options_json` is a camelCase `ScanProposalOptions` object
33    /// (`{}` for the defaults; `scanToModel` maps the report's frame into the
34    /// IFC model frame). Returns the UTF-8 JSON `ScanProposalReport` in the
35    /// model frame (Z up, metres). Pure: loads and changes nothing.
36    #[wasm_bindgen(js_name = proposeScanElements)]
37    pub fn propose_scan_elements(&self, report_json: &str, options_json: &str) -> Result<Vec<u8>, JsError> {
38        propose_json(report_json, options_json).map_err(|message| JsError::new(&message))
39    }
40}
41
42#[cfg(test)]
43mod tests {
44    use super::propose_json;
45    use crate::api::scan_segmentation::segment_json;
46
47    #[test]
48    fn issue_6894_proposal_binding_round_trips_a_segmentation_report_and_is_strict() {
49        let empty = String::from_utf8(segment_json(&[0., 0., 0.], "{}").unwrap()).unwrap();
50        let report: serde_json::Value = serde_json::from_slice(&propose_json(&empty, "{}").unwrap()).unwrap();
51        assert_eq!(report["algorithm"], "ifclite-scan-proposals-v1");
52        assert_eq!(report["proposals"].as_array().map(Vec::len), Some(0));
53        assert!(propose_json(&empty, r#"{"surprise":1}"#).unwrap_err().contains("Invalid scan proposal options"));
54        assert!(propose_json("{}", "{}").unwrap_err().contains("Invalid scan segmentation report"));
55        assert!(propose_json(&empty, &" ".repeat(64 * 1024 + 1)).unwrap_err().contains("64 KiB"));
56        assert!(propose_json(&empty, r#"{"scanToModel":[-1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]}"#).unwrap_err().contains("reflect"));
57    }
58}