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;