Skip to main content

ifc_lite_core/
fast_parse.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//! Fast Direct Parsing Module
6//!
7//! Provides zero-allocation parsing for coordinate lists and index arrays.
8//! This bypasses the Token/AttributeValue pipeline for massive speedups
9//! on tessellation-heavy IFC files.
10//!
11//! Performance: 3-5x faster than standard path for IfcTriangulatedFaceSet
12
13/// Check if byte is a digit, minus sign, or decimal point (start of number)
14#[inline(always)]
15fn is_number_start(b: u8) -> bool {
16    b.is_ascii_digit() || b == b'-' || b == b'.'
17}
18
19/// Estimate number of floats in coordinate data
20#[inline]
21fn estimate_float_count(bytes: &[u8]) -> usize {
22    // Rough estimate: ~8 bytes per float on average (including delimiters)
23    bytes.len() / 8
24}
25
26/// Estimate number of integers in index data
27#[inline]
28fn estimate_int_count(bytes: &[u8]) -> usize {
29    // Rough estimate: ~4 bytes per integer on average
30    bytes.len() / 4
31}
32
33/// Parse coordinate list directly from raw bytes to `Vec<f32>`
34///
35/// This parses IFC coordinate data like:
36/// `((0.,0.,150.),(0.,40.,140.),...)`
37///
38/// Returns flattened f32 array: [x0, y0, z0, x1, y1, z1, ...]
39///
40/// # Performance
41/// - Zero intermediate allocations (no Token, no AttributeValue)
42/// - Uses fast-float for SIMD-accelerated parsing
43/// - Pre-allocates result vector
44#[inline]
45pub fn parse_coordinates_direct(bytes: &[u8]) -> Vec<f32> {
46    let mut result = Vec::with_capacity(estimate_float_count(bytes));
47    let mut pos = 0;
48    let len = bytes.len();
49
50    while pos < len {
51        // Skip to next number using SIMD-accelerated search
52        while pos < len && !is_number_start(bytes[pos]) {
53            pos += 1;
54        }
55        if pos >= len {
56            break;
57        }
58
59        // Parse float directly
60        match fast_float2::parse_partial::<f32, _>(&bytes[pos..]) {
61            Ok((value, consumed)) if consumed > 0 => {
62                result.push(value);
63                pos += consumed;
64            }
65            _ => {
66                // Skip this character and continue
67                pos += 1;
68            }
69        }
70    }
71
72    result
73}
74
75/// Parse coordinate list directly from raw bytes to `Vec<f64>`
76///
77/// Same as parse_coordinates_direct but with f64 precision.
78#[inline]
79pub fn parse_coordinates_direct_f64(bytes: &[u8]) -> Vec<f64> {
80    let mut result = Vec::with_capacity(estimate_float_count(bytes));
81    let mut pos = 0;
82    let len = bytes.len();
83
84    while pos < len {
85        while pos < len && !is_number_start(bytes[pos]) {
86            pos += 1;
87        }
88        if pos >= len {
89            break;
90        }
91
92        match fast_float2::parse_partial::<f64, _>(&bytes[pos..]) {
93            Ok((value, consumed)) if consumed > 0 => {
94                result.push(value);
95                pos += consumed;
96            }
97            _ => {
98                pos += 1;
99            }
100        }
101    }
102
103    result
104}
105
106/// Parse index list directly from raw bytes to `Vec<u32>`
107///
108/// This parses IFC face index data like:
109/// `((1,2,3),(2,1,4),...)`
110///
111/// Automatically converts from 1-based IFC indices to 0-based.
112///
113/// # Performance
114/// - Zero intermediate allocations
115/// - Uses inline integer parsing
116#[inline]
117pub fn parse_indices_direct(bytes: &[u8]) -> Vec<u32> {
118    let mut result = Vec::with_capacity(estimate_int_count(bytes));
119    let mut pos = 0;
120    let len = bytes.len();
121
122    while pos < len {
123        // Skip to next digit
124        while pos < len && !bytes[pos].is_ascii_digit() {
125            pos += 1;
126        }
127        if pos >= len {
128            break;
129        }
130
131        // Parse integer inline (avoiding any allocation). Use CHECKED
132        // arithmetic so a pathologically large index in malformed input
133        // SATURATES to u32::MAX — an obviously out-of-range vertex the
134        // downstream bounds checks drop — instead of WRAPPING modulo 2^32 to an
135        // arbitrary, valid-looking (wrong) vertex. Digits keep being consumed
136        // after overflow so `pos` still advances past the whole number.
137        let mut value: u32 = 0;
138        let mut overflowed = false;
139        while pos < len && bytes[pos].is_ascii_digit() {
140            if !overflowed {
141                match value
142                    .checked_mul(10)
143                    .and_then(|v| v.checked_add((bytes[pos] - b'0') as u32))
144                {
145                    Some(v) => value = v,
146                    None => overflowed = true,
147                }
148            }
149            pos += 1;
150        }
151        if overflowed {
152            value = u32::MAX;
153        }
154
155        // Convert from 1-based to 0-based. NOTE: after saturation this yields
156        // u32::MAX - 1, while schema_gen's to_zero_based yields u32::MAX —
157        // consumers must bounds-check (i >= vertex_count), never compare
158        // against a single sentinel value.
159        result.push(value.saturating_sub(1));
160    }
161
162    result
163}
164
165/// Parse a single entity's coordinate list attribute
166///
167/// Takes the raw bytes of an entity line like:
168/// `#78=IFCCARTESIANPOINTLIST3D(((0.,0.,150.),(0.,40.,140.),...));`
169///
170/// And extracts just the coordinate data.
171#[inline]
172pub fn extract_coordinate_list_from_entity(bytes: &[u8]) -> Option<Vec<f32>> {
173    // Find the opening '((' which starts the coordinate list
174    let start = memchr::memmem::find(bytes, b"((")?;
175
176    // Find matching closing '))'
177    let end = memchr::memmem::rfind(bytes, b"))")?;
178
179    if end <= start {
180        return None;
181    }
182
183    // Parse the coordinate data
184    Some(parse_coordinates_direct(&bytes[start..end + 2]))
185}
186
187/// Parse face indices from IfcTriangulatedFaceSet entity
188///
189/// Finds the CoordIndex attribute (4th attribute, 0-indexed as 3)
190/// in an entity like:
191/// `#77=IFCTRIANGULATEDFACESET(#78,$,$,((1,2,3),(2,1,4),...),$);`
192#[inline]
193pub fn extract_face_indices_from_entity(bytes: &[u8]) -> Option<Vec<u32>> {
194    // Count commas to find the 4th attribute (CoordIndex)
195    // Format: IFCTRIANGULATEDFACESET(Coordinates,Normals,Closed,CoordIndex,PnIndex)
196    let mut paren_depth = 0;
197    let mut comma_count = 0;
198    let mut attr_start = None;
199    let mut attr_end = None;
200
201    for (i, &b) in bytes.iter().enumerate() {
202        match b {
203            b'(' => {
204                if paren_depth == 1 && comma_count == 3 && attr_start.is_none() {
205                    attr_start = Some(i);
206                }
207                paren_depth += 1;
208            }
209            b')' => {
210                paren_depth -= 1;
211                if paren_depth == 1
212                    && comma_count == 3
213                    && attr_start.is_some()
214                    && attr_end.is_none()
215                {
216                    attr_end = Some(i + 1);
217                }
218            }
219            b',' if paren_depth == 1 => {
220                if comma_count == 3 && attr_end.is_none() && attr_start.is_some() {
221                    attr_end = Some(i);
222                }
223                comma_count += 1;
224            }
225            _ => {}
226        }
227    }
228
229    let start = attr_start?;
230    let end = attr_end?;
231
232    if end <= start {
233        return None;
234    }
235
236    Some(parse_indices_direct(&bytes[start..end]))
237}
238
239/// Fast path checker - determines if entity type benefits from direct parsing
240#[inline]
241pub fn should_use_fast_path(type_name: &str) -> bool {
242    matches!(
243        type_name.to_uppercase().as_str(),
244        "IFCCARTESIANPOINTLIST3D"
245            | "IFCTRIANGULATEDFACESET"
246            | "IFCTRIANGULATEDIRREGULARNETWORK"
247            | "IFCPOLYGONALFACESET"
248            | "IFCINDEXEDPOLYGONALFACE"
249    )
250}
251
252/// Extract entity type name from raw bytes
253///
254/// From `#77=IFCTRIANGULATEDFACESET(...)` extracts `IFCTRIANGULATEDFACESET`
255#[inline]
256pub fn extract_entity_type_name(bytes: &[u8]) -> Option<&str> {
257    // Find '=' position
258    let eq_pos = bytes.iter().position(|&b| b == b'=')?;
259    // Find '(' position after '='
260    let paren_pos = bytes[eq_pos..].iter().position(|&b| b == b'(')?;
261    let type_start = eq_pos + 1;
262    let type_end = eq_pos + paren_pos;
263
264    if type_end <= type_start {
265        return None;
266    }
267
268    std::str::from_utf8(&bytes[type_start..type_end]).ok()
269}
270
271/// Extract the first entity reference from an entity's first attribute
272///
273/// From `#77=IFCTRIANGULATEDFACESET(#78,...)` extracts `78`
274#[inline]
275pub fn extract_first_entity_ref(bytes: &[u8]) -> Option<u32> {
276    // Find opening paren
277    let paren_pos = bytes.iter().position(|&b| b == b'(')?;
278    let content = &bytes[paren_pos + 1..];
279
280    // Find '#' which marks entity reference
281    let hash_pos = content.iter().position(|&b| b == b'#')?;
282    let id_start = hash_pos + 1;
283
284    // Parse the ID number
285    let mut id: u32 = 0;
286    let mut i = id_start;
287    while i < content.len() && content[i].is_ascii_digit() {
288        id = id.wrapping_mul(10).wrapping_add((content[i] - b'0') as u32);
289        i += 1;
290    }
291
292    if i > id_start {
293        Some(id)
294    } else {
295        None
296    }
297}
298
299/// Mesh data for fast path processing (avoiding full Mesh struct dependency)
300#[derive(Debug, Clone)]
301pub struct FastMeshData {
302    pub positions: Vec<f32>,
303    pub indices: Vec<u32>,
304}
305
306/// Process IfcTriangulatedFaceSet directly from raw bytes
307///
308/// This completely bypasses the Token/AttributeValue pipeline for
309/// maximum performance on tessellation geometry.
310///
311/// # Arguments
312/// * `faceset_bytes` - Raw bytes of the IfcTriangulatedFaceSet entity
313/// * `get_entity_bytes` - Function to retrieve raw bytes for a given entity ID
314///
315/// # Returns
316/// FastMeshData with positions and indices, or None if parsing fails
317#[inline]
318pub fn process_triangulated_faceset_direct<F>(
319    faceset_bytes: &[u8],
320    get_entity_bytes: F,
321) -> Option<FastMeshData>
322where
323    F: Fn(u32) -> Option<Vec<u8>>,
324{
325    // Extract coordinate entity reference from first attribute
326    let coord_entity_id = extract_first_entity_ref(faceset_bytes)?;
327
328    // Get raw bytes of coordinate list entity
329    let coord_bytes = get_entity_bytes(coord_entity_id)?;
330
331    // Parse coordinates directly
332    let positions = parse_coordinates_direct(&coord_bytes);
333
334    // Extract and parse indices from attribute 3 (CoordIndex)
335    let indices = extract_face_indices_from_entity(faceset_bytes)?;
336
337    Some(FastMeshData { positions, indices })
338}
339
340/// Extract entity IDs from a list attribute without full parsing
341///
342/// From `(#1,#2,#3)` extracts `[1, 2, 3]`
343#[inline]
344pub fn extract_entity_refs_from_list(bytes: &[u8]) -> Vec<u32> {
345    let mut ids = Vec::with_capacity(16);
346    let mut i = 0;
347    let len = bytes.len();
348
349    while i < len {
350        // Find next '#'
351        while i < len && bytes[i] != b'#' {
352            i += 1;
353        }
354        if i >= len {
355            break;
356        }
357        i += 1; // Skip '#'
358
359        // Parse ID
360        let mut id: u32 = 0;
361        while i < len && bytes[i].is_ascii_digit() {
362            id = id.wrapping_mul(10).wrapping_add((bytes[i] - b'0') as u32);
363            i += 1;
364        }
365        if id > 0 {
366            ids.push(id);
367        }
368    }
369
370    ids
371}
372
373#[cfg(test)]
374#[path = "fast_parse_tests.rs"]
375mod tests;