1use super::IfcAPI;
26use ifc_lite_core::{build_entity_index, DecodedEntity, EntityDecoder, EntityScanner, IfcType};
27use ifc_lite_geometry::GeometryRouter;
28use wasm_bindgen::prelude::*;
29
30#[derive(Clone, Debug, PartialEq)]
37pub(crate) struct GridAxis3D {
38 pub grid_id: u32,
39 pub axis_id: u32,
40 pub tag: String,
41 pub start: [f32; 3],
42 pub end: [f32; 3],
43}
44
45pub(crate) fn extract_grid_axes(content: &str) -> Vec<GridAxis3D> {
49 let entity_index = build_entity_index(content);
50 let mut decoder = EntityDecoder::with_index(content, entity_index);
51
52 let router = GeometryRouter::with_units(content, &mut decoder);
55 let unit_scale = router.unit_scale();
56
57 let rtc = router.detect_rtc_offset_from_first_element(content, &mut decoder);
62
63 let mut out: Vec<GridAxis3D> = Vec::new();
64 let mut scanner = EntityScanner::new(content);
65 while let Some((id, type_name, start, end)) = scanner.next_entity() {
66 if type_name != "IFCGRID" {
67 continue;
68 }
69 let Ok(grid) = decoder.decode_at_with_id(id, start, end) else {
70 continue;
71 };
72 let matrix = router
75 .resolve_scaled_placement(&grid, &mut decoder)
76 .unwrap_or([
77 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
78 ]);
79 append_grid_axes(&grid, id, &mut decoder, unit_scale, &matrix, rtc, &mut out);
80 }
81 out
82}
83
84fn append_grid_axes(
88 grid: &DecodedEntity,
89 grid_id: u32,
90 decoder: &mut EntityDecoder,
91 unit_scale: f64,
92 matrix: &[f64; 16],
93 rtc: (f64, f64, f64),
94 out: &mut Vec<GridAxis3D>,
95) {
96 for axis_attr_idx in [7usize, 8, 9] {
97 let Some(axes_attr) = grid.get(axis_attr_idx) else {
98 continue;
99 };
100 let Ok(axes) = decoder.resolve_ref_list(axes_attr) else {
101 continue;
102 };
103 for axis in axes {
104 if axis.ifc_type != IfcType::IfcGridAxis {
105 continue;
106 }
107 let axis_id = axis.id;
108 let tag = axis
109 .get(0)
110 .and_then(|a| a.as_string())
111 .unwrap_or("")
112 .to_string();
113
114 let Some(curve_ref) = axis.get_ref(1) else {
116 continue;
117 };
118 let Ok(curve) = decoder.decode_by_id(curve_ref) else {
119 continue;
120 };
121 let Some((p0, p1)) = sample_axis_endpoints(&curve, decoder) else {
122 continue;
123 };
124
125 let start = to_render_frame(p0, unit_scale, matrix, rtc);
126 let end = to_render_frame(p1, unit_scale, matrix, rtc);
127 out.push(GridAxis3D {
128 grid_id,
129 axis_id,
130 tag,
131 start,
132 end,
133 });
134 }
135 }
136}
137
138fn sample_axis_endpoints(
142 curve: &DecodedEntity,
143 decoder: &mut EntityDecoder,
144) -> Option<([f64; 3], [f64; 3])> {
145 if curve.ifc_type != IfcType::IfcPolyline {
146 return None;
147 }
148 let pts_attr = curve.get(0)?;
149 let points = decoder.resolve_ref_list(pts_attr).ok()?;
150 if points.len() < 2 {
151 return None;
152 }
153 let extract = |pe: &DecodedEntity| -> Option<[f64; 3]> {
154 if pe.ifc_type != IfcType::IfcCartesianPoint {
155 return None;
156 }
157 let coords = pe.get(0)?.as_list()?;
158 let x = coords.first()?.as_float()?;
159 let y = coords.get(1)?.as_float()?;
160 let z = coords.get(2).and_then(|v| v.as_float()).unwrap_or(0.0);
162 Some([x, y, z])
163 };
164 let first = extract(&points[0])?;
165 let last = extract(&points[points.len() - 1])?;
166 Some((first, last))
167}
168
169fn to_render_frame(
176 p: [f64; 3],
177 unit_scale: f64,
178 matrix: &[f64; 16],
179 rtc: (f64, f64, f64),
180) -> [f32; 3] {
181 let (x, y, z) = (p[0] * unit_scale, p[1] * unit_scale, p[2] * unit_scale);
182 let wx = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12];
184 let wy = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13];
185 let wz = matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14];
186 let rx = wx - rtc.0;
188 let ry = wy - rtc.1;
189 let rz = wz - rtc.2;
190 [rx as f32, rz as f32, -ry as f32]
193}
194
195#[wasm_bindgen]
201pub struct GridAxisJs {
202 grid_id: u32,
203 axis_id: u32,
204 tag: String,
205 start: [f32; 3],
206 end: [f32; 3],
207}
208
209#[wasm_bindgen]
210impl GridAxisJs {
211 #[wasm_bindgen(getter, js_name = gridId)]
213 pub fn grid_id(&self) -> u32 {
214 self.grid_id
215 }
216
217 #[wasm_bindgen(getter, js_name = axisId)]
219 pub fn axis_id(&self) -> u32 {
220 self.axis_id
221 }
222
223 #[wasm_bindgen(getter)]
225 pub fn tag(&self) -> String {
226 self.tag.clone()
227 }
228
229 #[wasm_bindgen(getter)]
231 pub fn start(&self) -> js_sys::Float32Array {
232 js_sys::Float32Array::from(&self.start[..])
233 }
234
235 #[wasm_bindgen(getter)]
237 pub fn end(&self) -> js_sys::Float32Array {
238 js_sys::Float32Array::from(&self.end[..])
239 }
240}
241
242impl From<&GridAxis3D> for GridAxisJs {
243 fn from(a: &GridAxis3D) -> Self {
244 Self {
245 grid_id: a.grid_id,
246 axis_id: a.axis_id,
247 tag: a.tag.clone(),
248 start: a.start,
249 end: a.end,
250 }
251 }
252}
253
254#[wasm_bindgen]
256pub struct GridAxisCollection {
257 axes: Vec<GridAxis3D>,
258}
259
260#[wasm_bindgen]
261impl GridAxisCollection {
262 #[wasm_bindgen(getter)]
264 pub fn length(&self) -> usize {
265 self.axes.len()
266 }
267
268 #[wasm_bindgen(getter, js_name = isEmpty)]
270 pub fn is_empty(&self) -> bool {
271 self.axes.is_empty()
272 }
273
274 #[wasm_bindgen(js_name = getAxis)]
276 pub fn get_axis(&self, index: usize) -> Option<GridAxisJs> {
277 self.axes.get(index).map(GridAxisJs::from)
278 }
279}
280
281#[wasm_bindgen]
286impl IfcAPI {
287 #[wasm_bindgen(js_name = parseGridLines)]
295 pub fn parse_grid_lines(&self, content: String) -> js_sys::Float32Array {
296 let axes = extract_grid_axes(&content);
297 let mut verts: Vec<f32> = Vec::with_capacity(axes.len() * 6);
298 for a in &axes {
299 verts.extend_from_slice(&a.start);
300 verts.extend_from_slice(&a.end);
301 }
302 js_sys::Float32Array::from(&verts[..])
303 }
304
305 #[wasm_bindgen(js_name = parseGridAxes)]
309 pub fn parse_grid_axes(&self, content: String) -> GridAxisCollection {
310 GridAxisCollection {
311 axes: extract_grid_axes(&content),
312 }
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 const LOCAL_GRID: &str = r#"ISO-10303-21;
324HEADER;
325FILE_DESCRIPTION((''),'2;1');
326FILE_NAME('','',(''),(''),'','','');
327FILE_SCHEMA(('IFC4'));
328ENDSEC;
329DATA;
330#1=IFCCARTESIANPOINT((0.,0.,0.));
331#2=IFCDIRECTION((0.,0.,1.));
332#3=IFCDIRECTION((1.,0.,0.));
333#4=IFCAXIS2PLACEMENT3D(#1,#2,#3);
334#5=IFCLOCALPLACEMENT($,#4);
335#10=IFCCARTESIANPOINT((0.,0.));
336#11=IFCCARTESIANPOINT((10.,0.));
337#12=IFCPOLYLINE((#10,#11));
338#13=IFCGRIDAXIS('A',#12,.T.);
339#20=IFCGRID('0aBcDeFgHiJkLmNoPqRsT0',$,'Grid',$,$,#5,$,(#13),$,$);
340ENDSEC;
341END-ISO-10303-21;
342"#;
343
344 #[test]
345 fn extracts_local_grid_axis() {
346 let axes = extract_grid_axes(LOCAL_GRID);
347 assert_eq!(axes.len(), 1, "expected one grid axis");
348 let a = &axes[0];
349 assert_eq!(a.tag, "A", "axis tag preserved");
350 assert!(a.start[0].abs() < 1e-4, "start x≈0, got {}", a.start[0]);
352 assert!(a.start[1].abs() < 1e-4, "start y≈0, got {}", a.start[1]);
353 assert!(a.start[2].abs() < 1e-4, "start z≈0, got {}", a.start[2]);
354 assert!(
356 (a.end[0] - 10.0).abs() < 1e-3,
357 "end renderer-x ≈10, got {}",
358 a.end[0]
359 );
360 assert!(a.end[1].abs() < 1e-3, "end elevation ≈0, got {}", a.end[1]);
361 }
362
363 #[test]
364 fn flat_line_list_is_even_xyz_triples() {
365 let axes = extract_grid_axes(LOCAL_GRID);
368 let mut verts: Vec<f32> = Vec::new();
369 for a in &axes {
370 verts.extend_from_slice(&a.start);
371 verts.extend_from_slice(&a.end);
372 }
373 assert!(!verts.is_empty(), "grid must emit line vertices");
374 assert_eq!(verts.len() % 3, 0, "vertices must be xyz triples");
375 assert_eq!((verts.len() / 3) % 2, 0, "line-list = even vertex count");
376 assert_eq!(verts.len(), 6, "one axis → 6 floats");
378 }
379
380 #[test]
381 fn empty_for_no_grid() {
382 let none = "ISO-10303-21;\nHEADER;\nFILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;\nENDSEC;\nEND-ISO-10303-21;\n";
383 assert!(extract_grid_axes(none).is_empty());
384 }
385
386 #[test]
387 fn georeferenced_grid_rebased_near_origin() {
388 let content = r#"ISO-10303-21;
392HEADER;
393FILE_DESCRIPTION((''),'2;1');
394FILE_NAME('','',(''),(''),'','','');
395FILE_SCHEMA(('IFC4'));
396ENDSEC;
397DATA;
398#1=IFCCARTESIANPOINT((0.,0.,0.));
399#2=IFCDIRECTION((0.,0.,1.));
400#3=IFCDIRECTION((1.,0.,0.));
401#4=IFCAXIS2PLACEMENT3D(#1,#2,#3);
402#5=IFCLOCALPLACEMENT($,#4);
403/* a wall far out at survey coords so RTC detection trips (>10 km) */
404#6=IFCCARTESIANPOINT((10400000.,2000000.,0.));
405#7=IFCAXIS2PLACEMENT3D(#6,#2,#3);
406#8=IFCLOCALPLACEMENT($,#7);
407#9=IFCPRODUCTDEFINITIONSHAPE($,$,(#41));
408#40=IFCCARTESIANPOINT((10400000.,2000000.,0.));
409#41=IFCSHAPEREPRESENTATION($,'Body','Curve2D',(#42));
410#42=IFCPOLYLINE((#40,#40));
411#43=IFCWALL('1WaLLWaLLWaLLWaLLWaLL00',$,'W',$,$,#8,#9,$,$);
412/* grid placed at the same survey frame */
413#50=IFCCARTESIANPOINT((10400000.,2000000.,0.));
414#51=IFCAXIS2PLACEMENT3D(#50,#2,#3);
415#52=IFCLOCALPLACEMENT($,#51);
416#10=IFCCARTESIANPOINT((0.,0.));
417#11=IFCCARTESIANPOINT((10.,0.));
418#12=IFCPOLYLINE((#10,#11));
419#13=IFCGRIDAXIS('A',#12,.T.);
420#20=IFCGRID('0aBcDeFgHiJkLmNoPqRsT0',$,'Grid',$,$,#52,$,(#13),$,$);
421ENDSEC;
422END-ISO-10303-21;
423"#;
424 let axes = extract_grid_axes(content);
425 assert_eq!(axes.len(), 1, "expected one grid axis");
426 let a = &axes[0];
427 for c in a.start.iter().chain(a.end.iter()) {
430 assert!(
431 c.abs() < 1000.0,
432 "render-frame coord must be near origin after RTC, got {c}"
433 );
434 }
435 }
436}