ifc_lite_wasm/api/
alignment_lines.rs1use super::IfcAPI;
22use ifc_lite_core::{
23 build_entity_index, extract_length_unit_scale, EntityDecoder, EntityScanner, IfcType,
24};
25use ifc_lite_geometry::{AlignmentCurve, GeometryRouter};
26use wasm_bindgen::prelude::*;
27
28const SAMPLE_STEP_FILE_UNITS: f64 = 1.0;
33const MAX_SAMPLES: usize = 5_000;
34
35#[wasm_bindgen]
36impl IfcAPI {
37 #[wasm_bindgen(js_name = parseAlignmentLines)]
46 pub fn parse_alignment_lines(&self, content: String) -> js_sys::Float32Array {
47 let verts = extract_alignment_line_vertices(&content);
48 js_sys::Float32Array::from(&verts[..])
49 }
50}
51
52pub(crate) fn extract_alignment_line_vertices(content: &str) -> Vec<f32> {
54 let entity_index = build_entity_index(content);
55 let mut decoder = EntityDecoder::with_index(content, entity_index);
56
57 let mut project_scanner = EntityScanner::new(content);
60 let mut unit_scale = 1.0_f64;
61 while let Some((id, type_name, _, _)) = project_scanner.next_entity() {
62 if type_name == "IFCPROJECT" {
63 if let Ok(s) = extract_length_unit_scale(&mut decoder, id) {
64 unit_scale = s;
65 }
66 break;
67 }
68 }
69
70 let router = GeometryRouter::with_scale(unit_scale);
74 let rtc = router.detect_rtc_offset_from_first_element(content, &mut decoder);
75
76 let mut out: Vec<f32> = Vec::new();
77 let mut scanner = EntityScanner::new(content);
78 while let Some((id, type_name, start, end)) = scanner.next_entity() {
79 if type_name != "IFCALIGNMENT" {
80 continue;
81 }
82 let Ok(entity) = decoder.decode_at_with_id(id, start, end) else {
83 continue;
84 };
85 let Some(axis) = locate_axis_curve(&entity, &mut decoder) else {
86 continue;
87 };
88 let Ok(Some(alignment)) = AlignmentCurve::parse(&axis, &mut decoder) else {
89 continue;
90 };
91 append_alignment_segments(&alignment, unit_scale, rtc, &mut out);
92 }
93 out
94}
95
96fn append_alignment_segments(
99 alignment: &AlignmentCurve,
100 unit_scale: f64,
101 rtc: (f64, f64, f64),
102 out: &mut Vec<f32>,
103) {
104 let length = alignment.horizontal_length();
105 if !(length.is_finite() && length > 0.0) {
106 return;
107 }
108
109 let raw_count = ((length / SAMPLE_STEP_FILE_UNITS).ceil() as usize).max(1);
110 let (step, count) = if raw_count > MAX_SAMPLES {
111 (length / MAX_SAMPLES as f64, MAX_SAMPLES + 1)
112 } else {
113 (SAMPLE_STEP_FILE_UNITS, raw_count + 1)
114 };
115
116 let mut pts: Vec<[f32; 3]> = Vec::with_capacity(count);
118 for i in 0..count {
119 let station = (i as f64 * step).min(length);
120 let o = alignment.evaluate(station).origin;
121 let mx = o.x * unit_scale - rtc.0;
123 let my = o.y * unit_scale - rtc.1;
124 let mz = o.z * unit_scale - rtc.2;
125 pts.push([mx as f32, mz as f32, -my as f32]);
128 }
129
130 for w in pts.windows(2) {
132 out.extend_from_slice(&w[0]);
133 out.extend_from_slice(&w[1]);
134 }
135}
136
137fn locate_axis_curve(
142 entity: &ifc_lite_core::DecodedEntity,
143 decoder: &mut EntityDecoder,
144) -> Option<ifc_lite_core::DecodedEntity> {
145 let alignment_curve = IfcType::from_str("IFCALIGNMENTCURVE");
146 for idx in [7usize, 8, 6] {
147 let Some(attr) = entity.get(idx) else { continue };
148 if attr.is_null() {
149 continue;
150 }
151 if let Ok(Some(resolved)) = decoder.resolve_ref(attr) {
152 if resolved.ifc_type == alignment_curve || resolved.ifc_type == IfcType::IfcPolyline {
153 return Some(resolved);
154 }
155 }
156 }
157 None
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 const CONTENT: &str = r#"ISO-10303-21;
167HEADER;
168FILE_DESCRIPTION((''),'2;1');
169FILE_NAME('','',(''),(''),'','','');
170FILE_SCHEMA(('IFC4X1'));
171ENDSEC;
172DATA;
173#1=IFCCARTESIANPOINT((0.,0.,0.));
174#2=IFCCARTESIANPOINT((10.,0.,0.));
175#3=IFCCARTESIANPOINT((10.,10.,0.));
176#4=IFCPOLYLINE((#1,#2,#3));
177#10=IFCALIGNMENT('0aBcDeFgHiJkLmNoPqRsT0',$,'Test Alignment',$,$,$,$,#4,$);
178ENDSEC;
179END-ISO-10303-21;
180"#;
181
182 #[test]
183 fn emits_line_list_for_polyline_alignment() {
184 let verts = extract_alignment_line_vertices(CONTENT);
185 assert!(!verts.is_empty(), "alignment must emit centerline vertices");
186 assert_eq!(verts.len() % 3, 0, "vertices must be xyz triples");
188 assert_eq!((verts.len() / 3) % 2, 0, "line-list = even vertex count");
189
190 assert!(verts[0].abs() < 1e-4, "start x≈0, got {}", verts[0]);
192 assert!(verts[1].abs() < 1e-4, "start y(elev)≈0, got {}", verts[1]);
193 assert!(verts[2].abs() < 1e-4, "start z≈0, got {}", verts[2]);
194
195 let mut max_x = f32::MIN;
199 let mut max_abs_z = 0.0_f32;
200 for v in verts.chunks_exact(3) {
201 assert!(v[1].abs() < 1e-3, "planar alignment elevation must be ~0");
202 max_x = max_x.max(v[0]);
203 max_abs_z = max_abs_z.max(v[2].abs());
204 }
205 assert!((max_x - 10.0).abs() < 0.5, "max renderer-x ≈10, got {max_x}");
206 assert!((max_abs_z - 10.0).abs() < 0.5, "max |renderer-z| ≈10, got {max_abs_z}");
207 }
208
209 #[test]
210 fn empty_for_no_alignment() {
211 let none = "ISO-10303-21;\nHEADER;\nFILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;\nENDSEC;\nEND-ISO-10303-21;\n";
212 assert!(extract_alignment_line_vertices(none).is_empty());
213 }
214}