Skip to main content

ifc_lite_core/decoder/
fast_buffers.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 STEP list decoding into caller-owned scratch (#3988). The owned-return
6//! accessors use the same fill routines; only scratch ownership differs.
7
8use super::{parse_cartesian_point_inline, EntityDecoder};
9
10impl EntityDecoder<'_> {
11    /// Extract entity reference IDs from a raw list attribute, in authored order.
12    #[inline]
13    pub fn get_entity_ref_list_fast(&mut self, entity_id: u32) -> Option<Vec<u32>> {
14        let mut ids = Vec::new();
15        self.get_entity_ref_list_fast_into(entity_id, &mut ids)?;
16        Some(ids)
17    }
18
19    /// Replace `ids` with the same list as [`Self::get_entity_ref_list_fast`],
20    /// reusing its allocation. Duplicates retain authored order; oversized refs
21    /// are dropped. On `None`, `ids` is empty. The caller owns the scratch lifetime.
22    #[inline]
23    pub fn get_entity_ref_list_fast_into(&mut self, entity_id: u32, ids: &mut Vec<u32>) -> Option<()> {
24        ids.clear();
25        let bytes = self.get_raw_bytes(entity_id)?;
26
27        // Pattern: IFCTYPE((#id1,#id2,...)); or IFCTYPE((#id1,#id2,...),other);
28        let mut i = 0;
29        let len = bytes.len();
30
31        // Skip to first '(' after '='
32        while i < len && bytes[i] != b'(' {
33            i += 1;
34        }
35        if i >= len {
36            return None;
37        }
38        i += 1; // Skip first '('
39
40        // Skip to second '(' for the list
41        while i < len && bytes[i] != b'(' {
42            i += 1;
43        }
44        if i >= len {
45            return None;
46        }
47        i += 1; // Skip second '('
48
49        // Parse entity IDs
50        ids.reserve(32);
51
52        while i < len {
53            // Skip whitespace and commas
54            while i < len
55                && (bytes[i] == b' ' || bytes[i] == b',' || bytes[i] == b'\n' || bytes[i] == b'\r')
56            {
57                i += 1;
58            }
59
60            if i >= len || bytes[i] == b')' {
61                break;
62            }
63
64            // Expect '#' followed by number
65            if bytes[i] == b'#' {
66                i += 1;
67                let start = i;
68                while i < len && bytes[i].is_ascii_digit() {
69                    i += 1;
70                }
71                if i > start {
72                    // Shared checked accumulator (#3421): an oversized id is dropped, not wrapped.
73                    if let Some(id) = crate::express_id::parse_express_id(&bytes[start..i]) {
74                        ids.push(id);
75                    }
76                }
77            } else {
78                i += 1; // Skip unknown character
79            }
80        }
81
82        if ids.is_empty() {
83            None
84        } else {
85            Some(())
86        }
87    }
88
89    /// Extract PolyLoop coordinates with the decoder's existing point cache.
90    #[inline]
91    pub fn get_polyloop_coords_cached(&mut self, entity_id: u32) -> Option<Vec<(f64, f64, f64)>> {
92        let mut coords = Vec::new();
93        self.get_polyloop_coords_cached_into(entity_id, &mut coords)?;
94        Some(coords)
95    }
96
97    /// Replace `coords` with [`Self::get_polyloop_coords_cached`]'s ordered result,
98    /// reusing its allocation and the same point-cache policy and counters.
99    /// Missing/oversized point refs invalidate the whole loop. On `None`, the
100    /// buffer is empty; resolved points remain cached, as with the owned accessor.
101    #[inline]
102    pub fn get_polyloop_coords_cached_into(
103        &mut self, entity_id: u32, coords: &mut Vec<(f64, f64, f64)>,
104    ) -> Option<()> {
105        coords.clear();
106        // Ensure index is built once
107        self.build_index();
108        let index = self.entity_index.as_ref()?;
109        let bytes_full = self.content;
110
111        // Get polyloop raw bytes
112        let (start, end) = index.lookup(entity_id)?;
113        let bytes = &bytes_full[start..end];
114
115        // IFCPOLYLOOP((#id1,#id2,#id3,...));
116        let mut i = 0;
117        let len = bytes.len();
118
119        // Skip to first '(' after '='
120        while i < len && bytes[i] != b'(' {
121            i += 1;
122        }
123        if i >= len {
124            return None;
125        }
126        i += 1; // Skip first '('
127
128        // Skip to second '(' for the point list
129        while i < len && bytes[i] != b'(' {
130            i += 1;
131        }
132        if i >= len {
133            return None;
134        }
135        i += 1; // Skip second '('
136
137        // Parse point IDs and fetch coordinates (with caching)
138        // CRITICAL: Track expected count to ensure all points are resolved
139        coords.reserve(8);
140        let mut expected_count = 0u32;
141
142        while i < len {
143            // Skip whitespace and commas
144            while i < len
145                && (bytes[i] == b' ' || bytes[i] == b',' || bytes[i] == b'\n' || bytes[i] == b'\r')
146            {
147                i += 1;
148            }
149
150            if i >= len || bytes[i] == b')' {
151                break;
152            }
153
154            // Expect '#' followed by number
155            if bytes[i] == b'#' {
156                i += 1;
157                let id_start = i;
158                while i < len && bytes[i].is_ascii_digit() {
159                    i += 1;
160                }
161                if i > id_start {
162                    expected_count += 1; // Count every point ID we encounter
163
164                    // Shared checked accumulator (#3421): `expected_count`
165                    // was already bumped, so a refused id here trips the
166                    // `coords.len() == expected_count` check below, same as
167                    // any other missing point.
168                    if let Some(point_id) =
169                        crate::express_id::parse_express_id(&bytes[id_start..i])
170                    {
171                        // Check cache first
172                        if let Some(&coord) = self.point_cache.get(&point_id) {
173                            self.point_cache_hits += 1;
174                            coords.push(coord);
175                        } else {
176                            // Not in cache - parse and cache
177                            if let Some((pt_start, pt_end)) = index.lookup(point_id) {
178                                if let Some(coord) =
179                                    parse_cartesian_point_inline(&bytes_full[pt_start..pt_end])
180                                {
181                                    self.point_cache_misses += 1;
182                                    self.point_cache.insert(point_id, coord);
183                                    coords.push(coord);
184                                }
185                            }
186                        }
187                    }
188                }
189            } else {
190                i += 1; // Skip unknown character
191            }
192        }
193
194        // CRITICAL: Return None if ANY point failed to resolve
195        // This matches the old behavior where missing points invalidated the whole polygon
196        if coords.len() >= 3 && coords.len() == expected_count as usize {
197            Some(())
198        } else {
199            coords.clear();
200            None
201        }
202    }
203}
204
205#[cfg(test)]
206#[path = "fast_buffers_tests.rs"]
207mod tests;