1use crate::generated::IfcType;
10use crate::parser::EntityScanner;
11use futures_core::Stream;
12use futures_util::stream;
13use std::pin::Pin;
14
15#[derive(Debug, Clone)]
17pub enum ParseEvent {
18 Started {
20 file_size: usize,
22 timestamp: f64,
24 },
25
26 EntityScanned {
28 id: u32,
30 ifc_type: IfcType,
32 position: usize,
34 },
35
36 GeometryReady {
38 id: u32,
40 vertex_count: usize,
42 triangle_count: usize,
44 },
45
46 Progress {
48 phase: String,
50 percent: f32,
52 entities_processed: usize,
54 total_entities: usize,
56 },
57
58 Completed {
60 duration_ms: f64,
62 entity_count: usize,
64 triangle_count: usize,
66 },
67
68 Error {
70 message: String,
72 position: Option<usize>,
74 },
75}
76
77#[derive(Debug, Clone)]
79pub struct StreamConfig {
80 pub progress_interval: usize,
82 pub skip_types: Vec<IfcType>,
84 pub only_types: Option<Vec<IfcType>>,
86}
87
88impl Default for StreamConfig {
89 fn default() -> Self {
90 Self {
91 progress_interval: 100,
92 skip_types: vec![
93 IfcType::IfcOwnerHistory,
94 IfcType::IfcPerson,
95 IfcType::IfcOrganization,
96 IfcType::IfcApplication,
97 ],
98 only_types: None,
99 }
100 }
101}
102
103pub fn parse_stream<T>(
105 content: &T,
106 config: StreamConfig,
107) -> Pin<Box<dyn Stream<Item = ParseEvent> + '_>>
108where
109 T: AsRef<[u8]> + ?Sized,
110{
111 let content = content.as_ref();
112 Box::pin(stream::unfold(
113 ParserState::new(content, config),
114 |mut state| async move { state.next_event().map(|event| (event, state)) },
115 ))
116}
117
118struct ParserState<'a> {
120 content: &'a [u8],
121 scanner: EntityScanner<'a>,
122 config: StreamConfig,
123 started: bool,
124 completed: bool,
125 start_time: f64,
126 entities_scanned: usize,
127 total_entities: usize,
128 triangles_generated: usize,
129}
130
131impl<'a> ParserState<'a> {
132 fn new(content: &'a [u8], config: StreamConfig) -> Self {
133 Self {
134 content,
135 scanner: EntityScanner::new(content),
136 config,
137 started: false,
138 completed: false,
139 start_time: 0.0,
140 entities_scanned: 0,
141 total_entities: 0,
142 triangles_generated: 0,
143 }
144 }
145
146 fn next_event(&mut self) -> Option<ParseEvent> {
147 if self.completed {
149 return None;
150 }
151
152 if !self.started {
154 self.started = true;
155 self.start_time = get_timestamp();
156 return Some(ParseEvent::Started {
157 file_size: self.content.len(),
158 timestamp: self.start_time,
159 });
160 }
161
162 loop {
166 let Some((id, type_name, start, _end)) = self.scanner.next_entity() else {
167 self.completed = true;
169 let duration_ms = get_timestamp() - self.start_time;
170 return Some(ParseEvent::Completed {
171 duration_ms,
172 entity_count: self.entities_scanned,
173 triangle_count: self.triangles_generated,
174 });
175 };
176
177 let ifc_type = IfcType::from_str(type_name);
179
180 if self.config.skip_types.contains(&ifc_type) {
182 continue; }
184
185 if let Some(ref only_types) = self.config.only_types {
187 if !only_types.contains(&ifc_type) {
188 continue; }
190 }
191
192 self.entities_scanned += 1;
193
194 let event = ParseEvent::EntityScanned {
196 id,
197 ifc_type,
198 position: start,
199 };
200
201 if self
203 .entities_scanned
204 .is_multiple_of(self.config.progress_interval)
205 {
206 return Some(ParseEvent::Progress {
209 phase: "Scanning entities".to_string(),
210 percent: 0.0, entities_processed: self.entities_scanned,
212 total_entities: self.total_entities,
213 });
214 }
215
216 return Some(event);
217 }
218 }
219}
220
221fn get_timestamp() -> f64 {
224 #[cfg(not(target_arch = "wasm32"))]
225 {
226 std::time::SystemTime::now()
227 .duration_since(std::time::UNIX_EPOCH)
228 .unwrap()
229 .as_secs_f64()
230 * 1000.0
231 }
232
233 #[cfg(target_arch = "wasm32")]
234 {
235 0.0
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use futures_util::StreamExt;
245
246 #[tokio::test]
247 async fn test_parse_stream_basic() {
248 let content = r#"
249#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
250#2=IFCWALL('guid2',$,$,$,$,$,$,$);
251#3=IFCDOOR('guid3',$,$,$,$,$,$,$);
252"#;
253
254 let config = StreamConfig::default();
255 let mut stream = parse_stream(content, config);
256
257 let mut events = Vec::new();
258 while let Some(event) = stream.next().await {
259 events.push(event);
260 }
261
262 assert!(events.len() >= 5);
264
265 match events[0] {
267 ParseEvent::Started { .. } => {}
268 _ => panic!("Expected Started event"),
269 }
270
271 match events.last().unwrap() {
273 ParseEvent::Completed { entity_count, .. } => {
274 assert_eq!(*entity_count, 3);
275 }
276 _ => panic!("Expected Completed event"),
277 }
278 }
279
280 #[tokio::test]
281 async fn test_parse_stream_skip_types() {
282 let content = r#"
283#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
284#2=IFCOWNERHISTORY('guid2',$,$,$,$,$,$,$);
285#3=IFCWALL('guid3',$,$,$,$,$,$,$);
286"#;
287
288 let config = StreamConfig {
289 skip_types: vec![IfcType::IfcOwnerHistory],
290 ..Default::default()
291 };
292
293 let mut stream = parse_stream(content, config);
294
295 let mut entity_count = 0;
296 while let Some(event) = stream.next().await {
297 if let ParseEvent::EntityScanned { .. } = event {
298 entity_count += 1;
299 }
300 }
301
302 assert_eq!(entity_count, 2);
304 }
305
306 #[tokio::test]
307 async fn test_parse_stream_only_types() {
308 let content = r#"
309#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
310#2=IFCWALL('guid2',$,$,$,$,$,$,$);
311#3=IFCDOOR('guid3',$,$,$,$,$,$,$);
312"#;
313
314 let config = StreamConfig {
315 skip_types: vec![],
316 only_types: Some(vec![IfcType::IfcWall]),
317 ..Default::default()
318 };
319
320 let mut stream = parse_stream(content, config);
321
322 let mut entity_count = 0;
323 while let Some(event) = stream.next().await {
324 if let ParseEvent::EntityScanned { .. } = event {
325 entity_count += 1;
326 }
327 }
328
329 assert_eq!(entity_count, 1);
331 }
332
333 #[tokio::test]
334 async fn test_parse_stream_skips_garbage_and_completes() {
335 let content = r#"
339#1=IFCPROJECT('g',$,$,$,$,$,$,$,$);
340this is not an entity line at all !!! ;;;
341#2=IFCWALL('g2',$,$,$,$,$,$,$);
342@%^&*() not valid step
343#3=IFCDOOR('g3',$,$,$,$,$,$,$);
344"#;
345
346 let mut stream = parse_stream(content, StreamConfig::default());
347
348 let mut entity_count = 0;
349 let mut completed = None;
350 while let Some(event) = stream.next().await {
351 match event {
352 ParseEvent::EntityScanned { .. } => entity_count += 1,
353 ParseEvent::Completed { entity_count: n, .. } => completed = Some(n),
354 _ => {}
355 }
356 }
357
358 assert_eq!(entity_count, 3, "scanner should skip garbage and find all 3");
359 assert_eq!(completed, Some(3), "stream must reach Completed, not truncate");
360 }
361}