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::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/// Parse event types emitted during streaming parse
17#[derive(Debug, Clone)]
18pub enum ParseEvent {
19    /// Parsing started
20    Started {
21        /// Total file size in bytes
22        file_size: usize,
23        /// Timestamp when parsing started
24        timestamp: f64,
25    },
26
27    /// Entity discovered during scanning
28    EntityScanned {
29        /// Entity ID
30        id: u32,
31        /// Entity type
32        ifc_type: IfcType,
33        /// Position in file
34        position: usize,
35    },
36
37    /// Geometry processing completed for an entity
38    GeometryReady {
39        /// Entity ID
40        id: u32,
41        /// Vertex count
42        vertex_count: usize,
43        /// Triangle count
44        triangle_count: usize,
45    },
46
47    /// Progress update
48    Progress {
49        /// Current phase (e.g., "Scanning", "Parsing", "Processing geometry")
50        phase: String,
51        /// Progress percentage (0-100)
52        percent: f32,
53        /// Entities processed so far
54        entities_processed: usize,
55        /// Total entities
56        total_entities: usize,
57    },
58
59    /// Parsing completed
60    Completed {
61        /// Total duration in milliseconds
62        duration_ms: f64,
63        /// Total entities parsed
64        entity_count: usize,
65        /// Total triangles generated
66        triangle_count: usize,
67    },
68
69    /// Error occurred
70    Error {
71        /// Error message
72        message: String,
73        /// Position where error occurred
74        position: Option<usize>,
75    },
76}
77
78/// Streaming parser configuration
79#[derive(Debug, Clone)]
80pub struct StreamConfig {
81    /// Yield progress events every N entities
82    pub progress_interval: usize,
83    /// Skip these entity types during scanning
84    pub skip_types: Vec<IfcType>,
85    /// Only process these entity types (if specified)
86    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
104/// Stream IFC file parsing with events
105pub 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
117/// Internal parser state for streaming
118struct 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        // Emit Started event on first call
145        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        // Scan for next entity
155        if let Some((id, type_name, start, _end)) = self.scanner.next_entity() {
156            // Parse entity type
157            let ifc_type = IfcType::from_str(type_name);
158
159            // Check if we should skip this type
160            if self.config.skip_types.contains(&ifc_type) {
161                return self.next_event(); // Skip to next
162            }
163
164            // Check if we should only process specific types
165            if let Some(ref only_types) = self.config.only_types {
166                if !only_types.contains(&ifc_type) {
167                    return self.next_event(); // Skip to next
168                }
169            }
170
171            self.entities_scanned += 1;
172
173            // Emit EntityScanned event
174            let event = ParseEvent::EntityScanned {
175                id,
176                ifc_type,
177                position: start,
178            };
179
180            // Check if we should emit progress
181            if self.entities_scanned % self.config.progress_interval == 0 {
182                // Note: In a real implementation, we'd estimate total_entities
183                // by doing a quick pre-scan or using file size heuristics
184                return Some(ParseEvent::Progress {
185                    phase: "Scanning entities".to_string(),
186                    percent: 0.0, // Would calculate based on position/file_size
187                    entities_processed: self.entities_scanned,
188                    total_entities: self.total_entities,
189                });
190            }
191
192            Some(event)
193        } else {
194            // No more entities - emit Completed event
195            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
205/// Get current timestamp (mock implementation for native Rust)
206/// In WASM, this would use web_sys::window().performance().now()
207fn 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        // In WASM, would use:
220        // web_sys::window().unwrap().performance().unwrap().now()
221        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        // Should have: Started, EntityScanned x3, Completed
247        assert!(events.len() >= 5);
248
249        // First event should be Started
250        match events[0] {
251            ParseEvent::Started { .. } => {}
252            _ => panic!("Expected Started event"),
253        }
254
255        // Last event should be Completed
256        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        // Should only get 2 entities (skip IfcOwnerHistory)
285        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![]; // Don't skip anything
298        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        // Should only get 1 entity (only IFCWALL)
310        assert_eq!(entity_count, 1);
311    }
312}