use crate::generated::IfcType;
use crate::parser::EntityScanner;
use futures_core::Stream;
use futures_util::stream;
use std::pin::Pin;
#[derive(Debug, Clone)]
pub enum ParseEvent {
Started {
file_size: usize,
timestamp: f64,
},
EntityScanned {
id: u32,
ifc_type: IfcType,
position: usize,
},
GeometryReady {
id: u32,
vertex_count: usize,
triangle_count: usize,
},
Progress {
phase: String,
percent: f32,
entities_processed: usize,
total_entities: usize,
},
Completed {
duration_ms: f64,
entity_count: usize,
triangle_count: usize,
},
Error {
message: String,
position: Option<usize>,
},
}
#[derive(Debug, Clone)]
pub struct StreamConfig {
pub progress_interval: usize,
pub skip_types: Vec<IfcType>,
pub only_types: Option<Vec<IfcType>>,
}
impl Default for StreamConfig {
fn default() -> Self {
Self {
progress_interval: 100,
skip_types: vec![
IfcType::IfcOwnerHistory,
IfcType::IfcPerson,
IfcType::IfcOrganization,
IfcType::IfcApplication,
],
only_types: None,
}
}
}
pub fn parse_stream(
content: &str,
config: StreamConfig,
) -> Pin<Box<dyn Stream<Item = ParseEvent> + '_>> {
Box::pin(stream::unfold(
ParserState::new(content, config),
|mut state| async move { state.next_event().map(|event| (event, state)) },
))
}
struct ParserState<'a> {
content: &'a str,
scanner: EntityScanner<'a>,
config: StreamConfig,
started: bool,
completed: bool,
start_time: f64,
entities_scanned: usize,
total_entities: usize,
triangles_generated: usize,
}
impl<'a> ParserState<'a> {
fn new(content: &'a str, config: StreamConfig) -> Self {
Self {
content,
scanner: EntityScanner::new(content),
config,
started: false,
completed: false,
start_time: 0.0,
entities_scanned: 0,
total_entities: 0,
triangles_generated: 0,
}
}
fn next_event(&mut self) -> Option<ParseEvent> {
if self.completed {
return None;
}
if !self.started {
self.started = true;
self.start_time = get_timestamp();
return Some(ParseEvent::Started {
file_size: self.content.len(),
timestamp: self.start_time,
});
}
if let Some((id, type_name, start, _end)) = self.scanner.next_entity() {
let ifc_type = IfcType::from_str(type_name);
if self.config.skip_types.contains(&ifc_type) {
return self.next_event(); }
if let Some(ref only_types) = self.config.only_types {
if !only_types.contains(&ifc_type) {
return self.next_event(); }
}
self.entities_scanned += 1;
let event = ParseEvent::EntityScanned {
id,
ifc_type,
position: start,
};
if self
.entities_scanned
.is_multiple_of(self.config.progress_interval)
{
return Some(ParseEvent::Progress {
phase: "Scanning entities".to_string(),
percent: 0.0, entities_processed: self.entities_scanned,
total_entities: self.total_entities,
});
}
Some(event)
} else {
self.completed = true;
let duration_ms = get_timestamp() - self.start_time;
Some(ParseEvent::Completed {
duration_ms,
entity_count: self.entities_scanned,
triangle_count: self.triangles_generated,
})
}
}
}
fn get_timestamp() -> f64 {
#[cfg(not(target_arch = "wasm32"))]
{
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs_f64()
* 1000.0
}
#[cfg(target_arch = "wasm32")]
{
0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures_util::StreamExt;
#[tokio::test]
async fn test_parse_stream_basic() {
let content = r#"
#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
#2=IFCWALL('guid2',$,$,$,$,$,$,$);
#3=IFCDOOR('guid3',$,$,$,$,$,$,$);
"#;
let config = StreamConfig::default();
let mut stream = parse_stream(content, config);
let mut events = Vec::new();
while let Some(event) = stream.next().await {
events.push(event);
}
assert!(events.len() >= 5);
match events[0] {
ParseEvent::Started { .. } => {}
_ => panic!("Expected Started event"),
}
match events.last().unwrap() {
ParseEvent::Completed { entity_count, .. } => {
assert_eq!(*entity_count, 3);
}
_ => panic!("Expected Completed event"),
}
}
#[tokio::test]
async fn test_parse_stream_skip_types() {
let content = r#"
#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
#2=IFCOWNERHISTORY('guid2',$,$,$,$,$,$,$);
#3=IFCWALL('guid3',$,$,$,$,$,$,$);
"#;
let config = StreamConfig {
skip_types: vec![IfcType::IfcOwnerHistory],
..Default::default()
};
let mut stream = parse_stream(content, config);
let mut entity_count = 0;
while let Some(event) = stream.next().await {
if let ParseEvent::EntityScanned { .. } = event {
entity_count += 1;
}
}
assert_eq!(entity_count, 2);
}
#[tokio::test]
async fn test_parse_stream_only_types() {
let content = r#"
#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
#2=IFCWALL('guid2',$,$,$,$,$,$,$);
#3=IFCDOOR('guid3',$,$,$,$,$,$,$);
"#;
let config = StreamConfig {
skip_types: vec![],
only_types: Some(vec![IfcType::IfcWall]),
..Default::default()
};
let mut stream = parse_stream(content, config);
let mut entity_count = 0;
while let Some(event) = stream.next().await {
if let ParseEvent::EntityScanned { .. } = event {
entity_count += 1;
}
}
assert_eq!(entity_count, 1);
}
}