use super::IfcAPI;
use ifc_lite_core::{
build_entity_index, extract_length_unit_scale, EntityDecoder, EntityScanner, IfcType,
};
use ifc_lite_geometry::{AlignmentCurve, GeometryRouter};
use wasm_bindgen::prelude::*;
const SAMPLE_STEP_FILE_UNITS: f64 = 1.0;
const MAX_SAMPLES: usize = 5_000;
#[wasm_bindgen]
impl IfcAPI {
#[wasm_bindgen(js_name = parseAlignmentLines)]
pub fn parse_alignment_lines(&self, content: String) -> js_sys::Float32Array {
let verts = extract_alignment_line_vertices(&content);
js_sys::Float32Array::from(&verts[..])
}
}
pub(crate) fn extract_alignment_line_vertices(content: &str) -> Vec<f32> {
let entity_index = build_entity_index(content);
let mut decoder = EntityDecoder::with_index(content, entity_index);
let mut project_scanner = EntityScanner::new(content);
let mut unit_scale = 1.0_f64;
while let Some((id, type_name, _, _)) = project_scanner.next_entity() {
if type_name == "IFCPROJECT" {
if let Ok(s) = extract_length_unit_scale(&mut decoder, id) {
unit_scale = s;
}
break;
}
}
let router = GeometryRouter::with_scale(unit_scale);
let rtc = router.detect_rtc_offset_from_first_element(content, &mut decoder);
let mut out: Vec<f32> = Vec::new();
let mut scanner = EntityScanner::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if type_name != "IFCALIGNMENT" {
continue;
}
let Ok(entity) = decoder.decode_at_with_id(id, start, end) else {
continue;
};
let Some(axis) = locate_axis_curve(&entity, &mut decoder) else {
continue;
};
let Ok(Some(alignment)) = AlignmentCurve::parse(&axis, &mut decoder) else {
continue;
};
append_alignment_segments(&alignment, unit_scale, rtc, &mut out);
}
out
}
fn append_alignment_segments(
alignment: &AlignmentCurve,
unit_scale: f64,
rtc: (f64, f64, f64),
out: &mut Vec<f32>,
) {
let length = alignment.horizontal_length();
if !(length.is_finite() && length > 0.0) {
return;
}
let raw_count = ((length / SAMPLE_STEP_FILE_UNITS).ceil() as usize).max(1);
let (step, count) = if raw_count > MAX_SAMPLES {
(length / MAX_SAMPLES as f64, MAX_SAMPLES + 1)
} else {
(SAMPLE_STEP_FILE_UNITS, raw_count + 1)
};
let mut pts: Vec<[f32; 3]> = Vec::with_capacity(count);
for i in 0..count {
let station = (i as f64 * step).min(length);
let o = alignment.evaluate(station).origin;
let mx = o.x * unit_scale - rtc.0;
let my = o.y * unit_scale - rtc.1;
let mz = o.z * unit_scale - rtc.2;
pts.push([mx as f32, mz as f32, -my as f32]);
}
for w in pts.windows(2) {
out.extend_from_slice(&w[0]);
out.extend_from_slice(&w[1]);
}
}
fn locate_axis_curve(
entity: &ifc_lite_core::DecodedEntity,
decoder: &mut EntityDecoder,
) -> Option<ifc_lite_core::DecodedEntity> {
let alignment_curve = IfcType::from_str("IFCALIGNMENTCURVE");
for idx in [7usize, 8, 6] {
let Some(attr) = entity.get(idx) else { continue };
if attr.is_null() {
continue;
}
if let Ok(Some(resolved)) = decoder.resolve_ref(attr) {
if resolved.ifc_type == alignment_curve || resolved.ifc_type == IfcType::IfcPolyline {
return Some(resolved);
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
const CONTENT: &str = r#"ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('','',(''),(''),'','','');
FILE_SCHEMA(('IFC4X1'));
ENDSEC;
DATA;
#1=IFCCARTESIANPOINT((0.,0.,0.));
#2=IFCCARTESIANPOINT((10.,0.,0.));
#3=IFCCARTESIANPOINT((10.,10.,0.));
#4=IFCPOLYLINE((#1,#2,#3));
#10=IFCALIGNMENT('0aBcDeFgHiJkLmNoPqRsT0',$,'Test Alignment',$,$,$,$,#4,$);
ENDSEC;
END-ISO-10303-21;
"#;
#[test]
fn emits_line_list_for_polyline_alignment() {
let verts = extract_alignment_line_vertices(CONTENT);
assert!(!verts.is_empty(), "alignment must emit centerline vertices");
assert_eq!(verts.len() % 3, 0, "vertices must be xyz triples");
assert_eq!((verts.len() / 3) % 2, 0, "line-list = even vertex count");
assert!(verts[0].abs() < 1e-4, "start x≈0, got {}", verts[0]);
assert!(verts[1].abs() < 1e-4, "start y(elev)≈0, got {}", verts[1]);
assert!(verts[2].abs() < 1e-4, "start z≈0, got {}", verts[2]);
let mut max_x = f32::MIN;
let mut max_abs_z = 0.0_f32;
for v in verts.chunks_exact(3) {
assert!(v[1].abs() < 1e-3, "planar alignment elevation must be ~0");
max_x = max_x.max(v[0]);
max_abs_z = max_abs_z.max(v[2].abs());
}
assert!((max_x - 10.0).abs() < 0.5, "max renderer-x ≈10, got {max_x}");
assert!((max_abs_z - 10.0).abs() < 0.5, "max |renderer-z| ≈10, got {max_abs_z}");
}
#[test]
fn empty_for_no_alignment() {
let none = "ISO-10303-21;\nHEADER;\nFILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;\nENDSEC;\nEND-ISO-10303-21;\n";
assert!(extract_alignment_line_vertices(none).is_empty());
}
}