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(
105 content: &str,
106 config: StreamConfig,
107) -> Pin<Box<dyn Stream<Item = ParseEvent> + '_>> {
108 Box::pin(stream::unfold(
109 ParserState::new(content, config),
110 |mut state| async move { state.next_event().map(|event| (event, state)) },
111 ))
112}
113
114struct ParserState<'a> {
116 content: &'a str,
117 scanner: EntityScanner<'a>,
118 config: StreamConfig,
119 started: bool,
120 completed: bool,
121 start_time: f64,
122 entities_scanned: usize,
123 total_entities: usize,
124 triangles_generated: usize,
125}
126
127impl<'a> ParserState<'a> {
128 fn new(content: &'a str, config: StreamConfig) -> Self {
129 Self {
130 content,
131 scanner: EntityScanner::new(content),
132 config,
133 started: false,
134 completed: false,
135 start_time: 0.0,
136 entities_scanned: 0,
137 total_entities: 0,
138 triangles_generated: 0,
139 }
140 }
141
142 fn next_event(&mut self) -> Option<ParseEvent> {
143 if self.completed {
145 return None;
146 }
147
148 if !self.started {
150 self.started = true;
151 self.start_time = get_timestamp();
152 return Some(ParseEvent::Started {
153 file_size: self.content.len(),
154 timestamp: self.start_time,
155 });
156 }
157
158 if let Some((id, type_name, start, _end)) = self.scanner.next_entity() {
160 let ifc_type = IfcType::from_str(type_name);
162
163 if self.config.skip_types.contains(&ifc_type) {
165 return self.next_event(); }
167
168 if let Some(ref only_types) = self.config.only_types {
170 if !only_types.contains(&ifc_type) {
171 return self.next_event(); }
173 }
174
175 self.entities_scanned += 1;
176
177 let event = ParseEvent::EntityScanned {
179 id,
180 ifc_type,
181 position: start,
182 };
183
184 if self
186 .entities_scanned
187 .is_multiple_of(self.config.progress_interval)
188 {
189 return Some(ParseEvent::Progress {
192 phase: "Scanning entities".to_string(),
193 percent: 0.0, entities_processed: self.entities_scanned,
195 total_entities: self.total_entities,
196 });
197 }
198
199 Some(event)
200 } else {
201 self.completed = true;
203 let duration_ms = get_timestamp() - self.start_time;
204 Some(ParseEvent::Completed {
205 duration_ms,
206 entity_count: self.entities_scanned,
207 triangle_count: self.triangles_generated,
208 })
209 }
210 }
211}
212
213fn get_timestamp() -> f64 {
216 #[cfg(not(target_arch = "wasm32"))]
217 {
218 std::time::SystemTime::now()
219 .duration_since(std::time::UNIX_EPOCH)
220 .unwrap()
221 .as_secs_f64()
222 * 1000.0
223 }
224
225 #[cfg(target_arch = "wasm32")]
226 {
227 0.0
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use futures_util::StreamExt;
237
238 #[tokio::test]
239 async fn test_parse_stream_basic() {
240 let content = r#"
241#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
242#2=IFCWALL('guid2',$,$,$,$,$,$,$);
243#3=IFCDOOR('guid3',$,$,$,$,$,$,$);
244"#;
245
246 let config = StreamConfig::default();
247 let mut stream = parse_stream(content, config);
248
249 let mut events = Vec::new();
250 while let Some(event) = stream.next().await {
251 events.push(event);
252 }
253
254 assert!(events.len() >= 5);
256
257 match events[0] {
259 ParseEvent::Started { .. } => {}
260 _ => panic!("Expected Started event"),
261 }
262
263 match events.last().unwrap() {
265 ParseEvent::Completed { entity_count, .. } => {
266 assert_eq!(*entity_count, 3);
267 }
268 _ => panic!("Expected Completed event"),
269 }
270 }
271
272 #[tokio::test]
273 async fn test_parse_stream_skip_types() {
274 let content = r#"
275#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
276#2=IFCOWNERHISTORY('guid2',$,$,$,$,$,$,$);
277#3=IFCWALL('guid3',$,$,$,$,$,$,$);
278"#;
279
280 let config = StreamConfig {
281 skip_types: vec![IfcType::IfcOwnerHistory],
282 ..Default::default()
283 };
284
285 let mut stream = parse_stream(content, config);
286
287 let mut entity_count = 0;
288 while let Some(event) = stream.next().await {
289 if let ParseEvent::EntityScanned { .. } = event {
290 entity_count += 1;
291 }
292 }
293
294 assert_eq!(entity_count, 2);
296 }
297
298 #[tokio::test]
299 async fn test_parse_stream_only_types() {
300 let content = r#"
301#1=IFCPROJECT('guid',$,$,$,$,$,$,$,$);
302#2=IFCWALL('guid2',$,$,$,$,$,$,$);
303#3=IFCDOOR('guid3',$,$,$,$,$,$,$);
304"#;
305
306 let config = StreamConfig {
307 skip_types: vec![],
308 only_types: Some(vec![IfcType::IfcWall]),
309 ..Default::default()
310 };
311
312 let mut stream = parse_stream(content, config);
313
314 let mut entity_count = 0;
315 while let Some(event) = stream.next().await {
316 if let ParseEvent::EntityScanned { .. } = event {
317 entity_count += 1;
318 }
319 }
320
321 assert_eq!(entity_count, 1);
323 }
324}