Skip to main content

ifc_lite_core/
streaming.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Streaming IFC Parser
6//!
7//! Progressive parsing with event callbacks for real-time processing.
8
9use crate::generated::IfcType;
10use crate::parser::EntityScanner;
11use futures_core::Stream;
12use futures_util::stream;
13use std::pin::Pin;
14
15/// Parse event types emitted during streaming parse
16#[derive(Debug, Clone)]
17pub enum ParseEvent {
18    /// Parsing started
19    Started {
20        /// Total file size in bytes
21        file_size: usize,
22        /// Timestamp when parsing started
23        timestamp: f64,
24    },
25
26    /// Entity discovered during scanning
27    EntityScanned {
28        /// Entity ID
29        id: u32,
30        /// Entity type
31        ifc_type: IfcType,
32        /// Position in file
33        position: usize,
34    },
35
36    /// Geometry processing completed for an entity
37    GeometryReady {
38        /// Entity ID
39        id: u32,
40        /// Vertex count
41        vertex_count: usize,
42        /// Triangle count
43        triangle_count: usize,
44    },
45
46    /// Progress update
47    Progress {
48        /// Current phase (e.g., "Scanning", "Parsing", "Processing geometry")
49        phase: String,
50        /// Progress percentage (0-100)
51        percent: f32,
52        /// Entities processed so far
53        entities_processed: usize,
54        /// Total entities
55        total_entities: usize,
56    },
57
58    /// Parsing completed
59    Completed {
60        /// Total duration in milliseconds
61        duration_ms: f64,
62        /// Total entities parsed
63        entity_count: usize,
64        /// Total triangles generated
65        triangle_count: usize,
66    },
67
68    /// Error occurred
69    Error {
70        /// Error message
71        message: String,
72        /// Position where error occurred
73        position: Option<usize>,
74    },
75}
76
77/// Streaming parser configuration
78#[derive(Debug, Clone)]
79pub struct StreamConfig {
80    /// Yield progress events every N entities
81    pub progress_interval: usize,
82    /// Skip these entity types during scanning
83    pub skip_types: Vec<IfcType>,
84    /// Only process these entity types (if specified)
85    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
103/// Stream IFC file parsing with events
104pub 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
114/// Internal parser state for streaming
115struct 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        // Stream has ended - CRITICAL: prevents infinite loop!
144        if self.completed {
145            return None;
146        }
147
148        // Emit Started event on first call
149        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        // Scan for next entity
159        if let Some((id, type_name, start, _end)) = self.scanner.next_entity() {
160            // Parse entity type
161            let ifc_type = IfcType::from_str(type_name);
162
163            // Check if we should skip this type
164            if self.config.skip_types.contains(&ifc_type) {
165                return self.next_event(); // Skip to next
166            }
167
168            // Check if we should only process specific types
169            if let Some(ref only_types) = self.config.only_types {
170                if !only_types.contains(&ifc_type) {
171                    return self.next_event(); // Skip to next
172                }
173            }
174
175            self.entities_scanned += 1;
176
177            // Emit EntityScanned event
178            let event = ParseEvent::EntityScanned {
179                id,
180                ifc_type,
181                position: start,
182            };
183
184            // Check if we should emit progress
185            if self
186                .entities_scanned
187                .is_multiple_of(self.config.progress_interval)
188            {
189                // Note: In a real implementation, we'd estimate total_entities
190                // by doing a quick pre-scan or using file size heuristics
191                return Some(ParseEvent::Progress {
192                    phase: "Scanning entities".to_string(),
193                    percent: 0.0, // Would calculate based on position/file_size
194                    entities_processed: self.entities_scanned,
195                    total_entities: self.total_entities,
196                });
197            }
198
199            Some(event)
200        } else {
201            // No more entities - emit Completed event and end stream
202            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
213/// Get current timestamp (mock implementation for native Rust)
214/// In WASM, this would use web_sys::window().performance().now()
215fn 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        // In WASM, would use:
228        // web_sys::window().unwrap().performance().unwrap().now()
229        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        // Should have: Started, EntityScanned x3, Completed
255        assert!(events.len() >= 5);
256
257        // First event should be Started
258        match events[0] {
259            ParseEvent::Started { .. } => {}
260            _ => panic!("Expected Started event"),
261        }
262
263        // Last event should be Completed
264        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        // Should only get 2 entities (skip IfcOwnerHistory)
295        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        // Should only get 1 entity (only IFCWALL)
322        assert_eq!(entity_count, 1);
323    }
324}