use super::IfcAPI;
use ifc_lite_core::{EntityScanner, ParseEvent};
use js_sys::{Function, Promise};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
fn is_relevant_metadata_type(type_name: &str) -> bool {
matches!(
type_name,
"IFCWALL"
| "IFCWALLSTANDARDCASE"
| "IFCDOOR"
| "IFCWINDOW"
| "IFCSLAB"
| "IFCCOLUMN"
| "IFCBEAM"
| "IFCROOF"
| "IFCSTAIR"
| "IFCSTAIRFLIGHT"
| "IFCRAILING"
| "IFCRAMP"
| "IFCRAMPFLIGHT"
| "IFCPLATE"
| "IFCMEMBER"
| "IFCCURTAINWALL"
| "IFCFOOTING"
| "IFCPILE"
| "IFCBUILDINGELEMENTPROXY"
| "IFCFURNISHINGELEMENT"
| "IFCFLOWSEGMENT"
| "IFCFLOWTERMINAL"
| "IFCFLOWCONTROLLER"
| "IFCFLOWFITTING"
| "IFCSPACE"
| "IFCOPENINGELEMENT"
| "IFCSITE"
| "IFCBUILDING"
| "IFCBUILDINGSTOREY"
| "IFCPROJECT"
| "IFCFACILITY"
| "IFCFACILITYPART"
| "IFCBRIDGE"
| "IFCBRIDGEPART"
| "IFCROAD"
| "IFCROADPART"
| "IFCRAILWAY"
| "IFCRAILWAYPART"
| "IFCMARINEFACILITY"
| "IFCMAPCONVERSION"
| "IFCPROJECTEDCRS"
| "IFCRELAGGREGATES"
| "IFCRELCONTAINEDINSPATIALSTRUCTURE"
| "IFCRELDEFINESBYTYPE"
| "IFCRELVOIDSELEMENT"
| "IFCRELFILLSELEMENT"
| "IFCRELCONNECTSPATHELEMENTS"
| "IFCRELCONNECTSELEMENTS"
| "IFCRELSPACEBOUNDARY"
| "IFCRELASSIGNSTOGROUP"
| "IFCRELASSIGNSTOPRODUCT"
| "IFCRELREFERENCEDINSPATIALSTRUCTURE"
| "IFCRELDEFINESBYPROPERTIES"
| "IFCPROPERTYSET"
| "IFCPROPERTYSINGLEVALUE"
| "IFCPROPERTYENUMERATEDVALUE"
| "IFCPROPERTYBOUNDEDVALUE"
| "IFCPROPERTYLISTVALUE"
| "IFCPROPERTYTABLEVALUE"
| "IFCPROPERTYREFERENCEVALUE"
| "IFCCOMPLEXPROPERTY"
| "IFCELEMENTQUANTITY"
| "IFCQUANTITYLENGTH"
| "IFCQUANTITYAREA"
| "IFCQUANTITYVOLUME"
| "IFCQUANTITYCOUNT"
| "IFCQUANTITYWEIGHT"
| "IFCQUANTITYTIME"
| "IFCRELASSOCIATESMATERIAL"
| "IFCRELASSOCIATESCLASSIFICATION"
| "IFCRELASSOCIATESDOCUMENT"
| "IFCCOVERING"
| "IFCANNOTATION"
| "IFCGRID"
) || type_name.ends_with("TYPE")
}
#[wasm_bindgen]
impl IfcAPI {
#[wasm_bindgen(js_name = parseStreaming)]
pub fn parse_streaming(&self, content: String, callback: Function) -> Promise {
use futures_util::StreamExt;
use ifc_lite_core::StreamConfig;
let mut content = Some(content);
let mut callback = Some(callback);
let promise = Promise::new(&mut |resolve, reject| {
let content = content.take().expect("content already taken");
let callback = callback.take().expect("callback already taken");
let reject = reject.clone();
spawn_local(async move {
let config = StreamConfig::default();
let mut stream = ifc_lite_core::parse_stream(&content, config);
while let Some(event) = stream.next().await {
let event_obj = parse_event_to_js(&event);
if let Err(e) = callback.call1(&JsValue::NULL, &event_obj) {
let _ = reject.call1(&JsValue::NULL, &e);
return;
}
if matches!(event, ParseEvent::Completed { .. }) {
if let Err(e) = resolve.call0(&JsValue::NULL) {
let _ = reject.call1(&JsValue::NULL, &e);
}
return;
}
}
if let Err(e) = resolve.call0(&JsValue::NULL) {
let _ = reject.call1(&JsValue::NULL, &e);
}
});
});
promise
}
#[wasm_bindgen]
pub fn parse(&self, content: String) -> Promise {
let mut content = Some(content);
let promise = Promise::new(&mut |resolve, reject| {
let content = content.take().expect("content already taken");
let reject = reject.clone();
spawn_local(async move {
let mut scanner = EntityScanner::new(&content);
let counts = scanner.count_by_type();
let total_entities: usize = counts.values().sum();
let result = js_sys::Object::new();
super::set_js_prop(
&result,
"entityCount",
&JsValue::from_f64(total_entities as f64),
);
super::set_js_prop(&result, "entityTypes", &super::counts_to_js(&counts));
if let Err(e) = resolve.call1(&JsValue::NULL, &result) {
let _ = reject.call1(&JsValue::NULL, &e);
}
});
});
promise
}
#[wasm_bindgen(js_name = scanEntitiesFast)]
pub fn scan_entities_fast(&self, content: &str) -> JsValue {
Self::scan_entities_fast_inner(content)
}
#[wasm_bindgen(js_name = scanEntitiesFastBytes)]
pub fn scan_entities_fast_bytes(&self, data: &[u8]) -> JsValue {
let content = unsafe { std::str::from_utf8_unchecked(data) };
Self::scan_entities_fast_inner(content)
}
fn scan_entities_fast_inner(content: &str) -> JsValue {
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::to_value;
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EntityRefJs {
express_id: u32,
#[serde(rename = "type")]
entity_type: String,
byte_offset: usize,
byte_length: usize,
line_number: usize,
}
let mut scanner = EntityScanner::new(content);
let mut refs = Vec::new();
let bytes = content.as_bytes();
let mut last_position = 0;
let mut line_count = 1;
let mut type_cache: rustc_hash::FxHashMap<&str, String> = rustc_hash::FxHashMap::default();
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if start > last_position {
line_count += bytes[last_position..start]
.iter()
.filter(|&&b| b == b'\n')
.count();
}
let entity_type = type_cache
.entry(type_name)
.or_insert_with(|| type_name.to_string())
.clone();
refs.push(EntityRefJs {
express_id: id,
entity_type,
byte_offset: start,
byte_length: end - start,
line_number: line_count,
});
last_position = end;
}
to_value(&refs).unwrap_or_else(|_| js_sys::Array::new().into())
}
#[wasm_bindgen(js_name = scanGeometryEntitiesFast)]
pub fn scan_geometry_entities_fast(&self, content: &str) -> JsValue {
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::to_value;
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct GeometryEntityRefJs {
express_id: u32,
#[serde(rename = "type")]
entity_type: String,
byte_offset: usize,
byte_length: usize,
}
let mut scanner = EntityScanner::new(content);
let mut refs = Vec::new();
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if ifc_lite_core::has_geometry_by_name(type_name) {
refs.push(GeometryEntityRefJs {
express_id: id,
entity_type: type_name.to_string(),
byte_offset: start,
byte_length: end - start,
});
}
}
to_value(&refs).unwrap_or_else(|_| js_sys::Array::new().into())
}
#[wasm_bindgen(js_name = scanRelevantEntitiesFastBytes)]
pub fn scan_relevant_entities_fast_bytes(&self, data: &[u8]) -> JsValue {
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::to_value;
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct EntityRefJs {
express_id: u32,
#[serde(rename = "type")]
entity_type: String,
byte_offset: usize,
byte_length: usize,
line_number: usize,
}
let content = unsafe { std::str::from_utf8_unchecked(data) };
let mut scanner = EntityScanner::new(content);
let mut refs = Vec::new();
let bytes = content.as_bytes();
let mut last_position = 0;
let mut line_count = 1;
let mut type_cache: rustc_hash::FxHashMap<&str, Option<String>> =
rustc_hash::FxHashMap::default();
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if start > last_position {
line_count += bytes[last_position..start]
.iter()
.filter(|&&b| b == b'\n')
.count();
}
let cached = type_cache
.entry(type_name)
.or_insert_with(|| {
let upper = type_name.to_ascii_uppercase();
if is_relevant_metadata_type(&upper) {
Some(upper)
} else {
None
}
});
if let Some(entity_type) = cached {
refs.push(EntityRefJs {
express_id: id,
entity_type: entity_type.clone(),
byte_offset: start,
byte_length: end - start,
line_number: line_count,
});
}
last_position = end;
}
to_value(&refs).unwrap_or_else(|_| js_sys::Array::new().into())
}
}
fn parse_event_to_js(event: &ParseEvent) -> JsValue {
let obj = js_sys::Object::new();
match event {
ParseEvent::Started {
file_size,
timestamp,
} => {
super::set_js_prop(&obj, "type", &"started".into());
super::set_js_prop(&obj, "fileSize", &(*file_size as f64).into());
super::set_js_prop(&obj, "timestamp", &(*timestamp).into());
}
ParseEvent::EntityScanned {
id,
ifc_type,
position,
} => {
super::set_js_prop(&obj, "type", &"entityScanned".into());
super::set_js_prop(&obj, "id", &(*id as f64).into());
super::set_js_prop(&obj, "ifcType", &ifc_type.as_str().into());
super::set_js_prop(&obj, "position", &(*position as f64).into());
}
ParseEvent::GeometryReady {
id,
vertex_count,
triangle_count,
} => {
super::set_js_prop(&obj, "type", &"geometryReady".into());
super::set_js_prop(&obj, "id", &(*id as f64).into());
super::set_js_prop(&obj, "vertexCount", &(*vertex_count as f64).into());
super::set_js_prop(&obj, "triangleCount", &(*triangle_count as f64).into());
}
ParseEvent::Progress {
phase,
percent,
entities_processed,
total_entities,
} => {
super::set_js_prop(&obj, "type", &"progress".into());
super::set_js_prop(&obj, "phase", &phase.as_str().into());
super::set_js_prop(&obj, "percent", &(*percent as f64).into());
super::set_js_prop(
&obj,
"entitiesProcessed",
&(*entities_processed as f64).into(),
);
super::set_js_prop(&obj, "totalEntities", &(*total_entities as f64).into());
}
ParseEvent::Completed {
duration_ms,
entity_count,
triangle_count,
} => {
super::set_js_prop(&obj, "type", &"completed".into());
super::set_js_prop(&obj, "durationMs", &(*duration_ms).into());
super::set_js_prop(&obj, "entityCount", &(*entity_count as f64).into());
super::set_js_prop(&obj, "triangleCount", &(*triangle_count as f64).into());
}
ParseEvent::Error { message, position } => {
super::set_js_prop(&obj, "type", &"error".into());
super::set_js_prop(&obj, "message", &message.as_str().into());
if let Some(pos) = position {
super::set_js_prop(&obj, "position", &(*pos as f64).into());
}
}
}
obj.into()
}