1use crate::error::{Error, Result};
10use crate::parser::EntityScanner;
11use crate::schema::IfcType;
12use futures_core::Stream;
13use futures_util::stream;
14use std::pin::Pin;
15
16#[derive(Debug, Clone)]
18pub enum ParseEvent {
19 Started {
21 file_size: usize,
23 timestamp: f64,
25 },
26
27 EntityScanned {
29 id: u32,
31 ifc_type: IfcType,
33 position: usize,
35 },
36
37 GeometryReady {
39 id: u32,
41 vertex_count: usize,
43 triangle_count: usize,
45 },
46
47 Progress {
49 phase: String,
51 percent: f32,
53 entities_processed: usize,
55 total_entities: usize,
57 },
58
59 Completed {
61 duration_ms: f64,
63 entity_count: usize,
65 triangle_count: usize,
67 },
68
69 Error {
71 message: String,
73 position: Option<usize>,
75 },
76}
77
78#[derive(Debug, Clone)]
80pub struct StreamConfig {
81 pub progress_interval: usize,
83 pub skip_types: Vec<IfcType>,
85 pub only_types: Option<Vec<IfcType>>,
87}
88
89impl Default for StreamConfig {
90 fn default() -> Self {
91 Self {
92 progress_interval: 100,
93 skip_types: vec![
94 IfcType::IfcOwnerHistory,
95 IfcType::IfcPerson,
96 IfcType::IfcOrganization,
97 IfcType::IfcApplication,
98 ],
99 only_types: None,
100 }
101 }
102}
103
104pub fn parse_stream(
106 content: &str,
107 config: StreamConfig,
108) -> Pin<Box<dyn Stream<Item = ParseEvent> + '_>> {
109 Box::pin(stream::unfold(
110 ParserState::new(content, config),
111 |mut state| async move {
112 state.next_event().map(|event| (event, state))
113 },
114 ))
115}
116
117struct ParserState<'a> {
119 content: &'a str,
120 scanner: EntityScanner<'a>,
121 config: StreamConfig,
122 started: bool,
123 start_time: f64,
124 entities_scanned: usize,
125 total_entities: usize,
126 triangles_generated: usize,
127}
128
129impl<'a> ParserState<'a> {
130 fn new(content: &'a str, config: StreamConfig) -> Self {
131 Self {
132 content,
133 scanner: EntityScanner::new(content),
134 config,
135 started: false,
136 start_time: 0.0,
137 entities_scanned: 0,
138 total_entities: 0,
139 triangles_generated: 0,
140 }
141 }
142
143 fn next_event(&mut self) -> Option<ParseEvent> {
144 if !self.started {
146 self.started = true;
147 self.start_time = get_timestamp();
148 return Some(ParseEvent::Started {
149 file_size: self.content.len(),
150 timestamp: self.start_time,
151 });
152 }
153
154 if let Some((id, type_name, start, _end)) = self.scanner.next_entity() {
156 let ifc_type = IfcType::from_str(type_name);
158
159 if self.config.skip_types.contains(&ifc_type) {
161 return self.next_event(); }
163
164 if let Some(ref only_types) = self.config.only_types {
166 if !only_types.contains(&ifc_type) {
167 return self.next_event(); }
169 }
170
171 self.entities_scanned += 1;
172
173 let event = ParseEvent::EntityScanned {
175 id,
176 ifc_type,
177 position: start,
178 };
179
180 if self.entities_scanned % self.config.progress_interval == 0 {
182 return Some(ParseEvent::Progress {
185 phase: "Scanning entities".to_string(),
186 percent: 0.0, entities_processed: self.entities_scanned,
188 total_entities: self.total_entities,
189 });
190 }
191
192 Some(event)
193 } else {
194 let duration_ms = get_timestamp() - self.start_time;
196 Some(ParseEvent::Completed {
197 duration_ms,
198 entity_count: self.entities_scanned,
199 triangle_count: self.triangles_generated,
200 })
201 }
202 }
203}
204
205fn get_timestamp() -> f64 {
208 #[cfg(not(target_arch = "wasm32"))]
209 {
210 std::time::SystemTime::now()
211 .duration_since(std::time::UNIX_EPOCH)
212 .unwrap()
213 .as_secs_f64()
214 * 1000.0
215 }
216
217 #[cfg(target_arch = "wasm32")]
218 {
219 0.0
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use futures_util::StreamExt;
229
230 #[tokio::test]
231 async fn test_parse_stream_basic() {
232 let content = r#"
233#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
234#2=IFCWALL('guid2',$,$,$,$,$,$,$);
235#3=IFCDOOR('guid3',$,$,$,$,$,$,$);
236"#;
237
238 let config = StreamConfig::default();
239 let mut stream = parse_stream(content, config);
240
241 let mut events = Vec::new();
242 while let Some(event) = stream.next().await {
243 events.push(event);
244 }
245
246 assert!(events.len() >= 5);
248
249 match events[0] {
251 ParseEvent::Started { .. } => {}
252 _ => panic!("Expected Started event"),
253 }
254
255 match events.last().unwrap() {
257 ParseEvent::Completed { entity_count, .. } => {
258 assert_eq!(*entity_count, 3);
259 }
260 _ => panic!("Expected Completed event"),
261 }
262 }
263
264 #[tokio::test]
265 async fn test_parse_stream_skip_types() {
266 let content = r#"
267#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
268#2=IFCOWNERHISTORY('guid2',$,$,$,$,$,$,$);
269#3=IFCWALL('guid3',$,$,$,$,$,$,$);
270"#;
271
272 let mut config = StreamConfig::default();
273 config.skip_types = vec![IfcType::IfcOwnerHistory];
274
275 let mut stream = parse_stream(content, config);
276
277 let mut entity_count = 0;
278 while let Some(event) = stream.next().await {
279 if let ParseEvent::EntityScanned { .. } = event {
280 entity_count += 1;
281 }
282 }
283
284 assert_eq!(entity_count, 2);
286 }
287
288 #[tokio::test]
289 async fn test_parse_stream_only_types() {
290 let content = r#"
291#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
292#2=IFCWALL('guid2',$,$,$,$,$,$,$);
293#3=IFCDOOR('guid3',$,$,$,$,$,$,$);
294"#;
295
296 let mut config = StreamConfig::default();
297 config.skip_types = vec![]; config.only_types = Some(vec![IfcType::IfcWall]);
299
300 let mut stream = parse_stream(content, config);
301
302 let mut entity_count = 0;
303 while let Some(event) = stream.next().await {
304 if let ParseEvent::EntityScanned { .. } = event {
305 entity_count += 1;
306 }
307 }
308
309 assert_eq!(entity_count, 1);
311 }
312}