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<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
118/// Internal parser state for streaming
119struct 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        // Stream has ended - CRITICAL: prevents infinite loop!
148        if self.completed {
149            return None;
150        }
151
152        // Emit Started event on first call
153        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        // Scan for the next entity, skipping filtered types iteratively. A
163        // `return self.next_event()` per skipped entity recursed once per skip
164        // and could overflow the stack on a long run of skip-listed records.
165        loop {
166            let Some((id, type_name, start, _end)) = self.scanner.next_entity() else {
167                // No more entities - emit Completed event and end stream
168                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            // Parse entity type
178            let ifc_type = IfcType::from_str(type_name);
179
180            // Check if we should skip this type
181            if self.config.skip_types.contains(&ifc_type) {
182                continue; // Skip to next
183            }
184
185            // Check if we should only process specific types
186            if let Some(ref only_types) = self.config.only_types {
187                if !only_types.contains(&ifc_type) {
188                    continue; // Skip to next
189                }
190            }
191
192            self.entities_scanned += 1;
193
194            // Emit EntityScanned event
195            let event = ParseEvent::EntityScanned {
196                id,
197                ifc_type,
198                position: start,
199            };
200
201            // Check if we should emit progress
202            if self
203                .entities_scanned
204                .is_multiple_of(self.config.progress_interval)
205            {
206                // Note: In a real implementation, we'd estimate total_entities
207                // by doing a quick pre-scan or using file size heuristics
208                return Some(ParseEvent::Progress {
209                    phase: "Scanning entities".to_string(),
210                    percent: 0.0, // Would calculate based on position/file_size
211                    entities_processed: self.entities_scanned,
212                    total_entities: self.total_entities,
213                });
214            }
215
216            return Some(event);
217        }
218    }
219}
220
221/// Get current timestamp (mock implementation for native Rust)
222/// In WASM, this would use web_sys::window().performance().now()
223fn 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        // In WASM, would use:
236        // web_sys::window().unwrap().performance().unwrap().now()
237        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        // Should have: Started, EntityScanned x3, Completed
263        assert!(events.len() >= 5);
264
265        // First event should be Started
266        match events[0] {
267            ParseEvent::Started { .. } => {}
268            _ => panic!("Expected Started event"),
269        }
270
271        // Last event should be Completed
272        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        // Should only get 2 entities (skip IfcOwnerHistory)
303        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        // Should only get 1 entity (only IFCWALL)
330        assert_eq!(entity_count, 1);
331    }
332
333    #[tokio::test]
334    async fn test_parse_stream_skips_garbage_and_completes() {
335        // Malformed lines interleaved with valid entities must not truncate the
336        // scan or hang: the scanner skips the garbage and still reaches the
337        // valid entities and a Completed event.
338        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}