1use crate::EntityScanner;
12use std::collections::HashSet;
13
14#[derive(Debug, Clone)]
16pub struct ModelBounds {
17 pub min_x: f64,
19 pub min_y: f64,
21 pub min_z: f64,
23 pub max_x: f64,
25 pub max_y: f64,
27 pub max_z: f64,
29 pub sample_count: usize,
31}
32
33impl ModelBounds {
34 pub fn new() -> Self {
36 Self {
37 min_x: f64::MAX,
38 min_y: f64::MAX,
39 min_z: f64::MAX,
40 max_x: f64::MIN,
41 max_y: f64::MIN,
42 max_z: f64::MIN,
43 sample_count: 0,
44 }
45 }
46
47 #[inline]
49 pub fn is_valid(&self) -> bool {
50 self.sample_count > 0
51 }
52
53 #[inline]
55 pub fn expand(&mut self, x: f64, y: f64, z: f64) {
56 self.min_x = self.min_x.min(x);
57 self.min_y = self.min_y.min(y);
58 self.min_z = self.min_z.min(z);
59 self.max_x = self.max_x.max(x);
60 self.max_y = self.max_y.max(y);
61 self.max_z = self.max_z.max(z);
62 self.sample_count += 1;
63 }
64
65 #[inline]
67 pub fn centroid(&self) -> (f64, f64, f64) {
68 if !self.is_valid() {
69 return (0.0, 0.0, 0.0);
70 }
71 (
72 (self.min_x + self.max_x) / 2.0,
73 (self.min_y + self.max_y) / 2.0,
74 (self.min_z + self.max_z) / 2.0,
75 )
76 }
77
78 #[inline]
80 pub fn has_large_coordinates(&self) -> bool {
81 const THRESHOLD: f64 = 10000.0; if !self.is_valid() {
83 return false;
84 }
85 self.min_x.abs() > THRESHOLD
86 || self.min_y.abs() > THRESHOLD
87 || self.max_x.abs() > THRESHOLD
88 || self.max_y.abs() > THRESHOLD
89 || self.min_z.abs() > THRESHOLD
90 || self.max_z.abs() > THRESHOLD
91 }
92
93 #[inline]
95 pub fn rtc_offset(&self) -> (f64, f64, f64) {
96 if self.has_large_coordinates() {
97 self.centroid()
98 } else {
99 (0.0, 0.0, 0.0)
100 }
101 }
102}
103
104impl Default for ModelBounds {
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110pub fn scan_model_bounds<T>(content: &T) -> ModelBounds
120where
121 T: AsRef<[u8]> + ?Sized,
122{
123 let content = content.as_ref();
124 let mut bounds = ModelBounds::new();
125
126 let mut scanner = EntityScanner::new(content);
128
129 while let Some((_id, type_name, start, end)) = scanner.next_entity() {
130 if type_name != "IFCCARTESIANPOINT" {
132 continue;
133 }
134
135 let entity_text = &content[start..end];
137
138 if let Some(coords) = extract_point_coordinates(entity_text) {
140 let x = coords.0;
141 let y = coords.1;
142 let z = coords.2.unwrap_or(0.0);
143
144 if x.is_finite() && y.is_finite() && z.is_finite() {
146 bounds.expand(x, y, z);
147 }
148 }
149 }
150
151 bounds
152}
153
154fn extract_point_coordinates<T>(bytes: &T) -> Option<(f64, f64, Option<f64>)>
157where
158 T: AsRef<[u8]> + ?Sized,
159{
160 let bytes = bytes.as_ref();
161 let text = std::str::from_utf8(bytes).ok()?;
162 let start = text.find("((")?;
164 let end = text.rfind("))")?;
165
166 if start >= end {
167 return None;
168 }
169
170 let coord_str = &text[start + 2..end];
171
172 let parts: Vec<&str> = coord_str.split(',').collect();
174
175 if parts.len() < 2 {
176 return None;
177 }
178
179 let x = parts[0].trim().parse::<f64>().ok()?;
180 let y = parts[1].trim().parse::<f64>().ok()?;
181 let z = if parts.len() > 2 {
182 parts[2].trim().parse::<f64>().ok()
183 } else {
184 None
185 };
186
187 Some((x, y, z))
188}
189
190pub fn scan_placement_bounds<T>(content: &T) -> ModelBounds
196where
197 T: AsRef<[u8]> + ?Sized,
198{
199 let content = content.as_ref();
200 let mut bounds = ModelBounds::new();
201 let mut scanner = EntityScanner::new(content);
202
203 let mut placement_point_ids: HashSet<u32> = HashSet::new();
205
206 while let Some((_id, type_name, start, end)) = scanner.next_entity() {
208 if type_name == "IFCAXIS2PLACEMENT3D" {
209 let entity_text = &content[start..end];
210 if let Some(ref_id) = extract_first_reference(entity_text) {
212 placement_point_ids.insert(ref_id);
213 }
214 }
215 if type_name == "IFCSITE" {
217 }
221 if type_name == "IFCCARTESIANPOINT" {
223 continue;
225 }
226 }
227
228 scanner = EntityScanner::new(content);
230 while let Some((id, type_name, start, end)) = scanner.next_entity() {
231 if type_name == "IFCCARTESIANPOINT" {
232 let is_placement_point = placement_point_ids.contains(&id);
234
235 let entity_text = &content[start..end];
238 if let Some(coords) = extract_point_coordinates(entity_text) {
239 let x = coords.0;
240 let y = coords.1;
241 let z = coords.2.unwrap_or(0.0);
242
243 if !x.is_finite() || !y.is_finite() || !z.is_finite() {
245 continue;
246 }
247
248 if is_placement_point || x.abs() > 1000.0 || y.abs() > 1000.0 || z.abs() > 1000.0 {
250 bounds.expand(x, y, z);
251 }
252 }
253 }
254 }
255
256 if !bounds.is_valid() {
258 return scan_model_bounds(content);
259 }
260
261 bounds
262}
263
264fn extract_first_reference<T>(bytes: &T) -> Option<u32>
267where
268 T: AsRef<[u8]> + ?Sized,
269{
270 let bytes = bytes.as_ref();
271 let text = std::str::from_utf8(bytes).ok()?;
272 let start = text.find('(')?;
274 let rest = &text[start + 1..];
275
276 let hash_pos = rest.find('#')?;
278 let after_hash = &rest[hash_pos + 1..];
279
280 let end_pos = after_hash
282 .find(|c: char| !c.is_ascii_digit())
283 .unwrap_or(after_hash.len());
284
285 if end_pos == 0 {
286 return None;
287 }
288
289 after_hash[..end_pos].parse().ok()
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[test]
297 fn test_bounds_creation() {
298 let bounds = ModelBounds::new();
299 assert!(!bounds.is_valid());
300 assert!(!bounds.has_large_coordinates());
301 }
302
303 #[test]
304 fn test_bounds_expand() {
305 let mut bounds = ModelBounds::new();
306 bounds.expand(100.0, 200.0, 50.0);
307 bounds.expand(150.0, 250.0, 75.0);
308
309 assert!(bounds.is_valid());
310 assert_eq!(bounds.min_x, 100.0);
311 assert_eq!(bounds.max_x, 150.0);
312 assert_eq!(bounds.min_y, 200.0);
313 assert_eq!(bounds.max_y, 250.0);
314
315 let centroid = bounds.centroid();
316 assert_eq!(centroid.0, 125.0);
317 assert_eq!(centroid.1, 225.0);
318 }
319
320 #[test]
321 fn test_large_coordinates_detection() {
322 let mut bounds = ModelBounds::new();
323 bounds.expand(2679012.0, 1247892.0, 432.0); assert!(bounds.has_large_coordinates());
326
327 let offset = bounds.rtc_offset();
328 assert_eq!(offset.0, 2679012.0);
329 assert_eq!(offset.1, 1247892.0);
330 }
331
332 #[test]
333 fn test_small_coordinates_no_shift() {
334 let mut bounds = ModelBounds::new();
335 bounds.expand(0.0, 0.0, 0.0);
336 bounds.expand(100.0, 100.0, 10.0);
337
338 assert!(!bounds.has_large_coordinates());
339
340 let offset = bounds.rtc_offset();
341 assert_eq!(offset.0, 0.0);
342 assert_eq!(offset.1, 0.0);
343 assert_eq!(offset.2, 0.0);
344 }
345
346 #[test]
347 fn test_extract_point_coordinates_3d() {
348 let text = "IFCCARTESIANPOINT((2679012.123,1247892.456,432.789))";
349 let coords = extract_point_coordinates(text).unwrap();
350
351 assert!((coords.0 - 2679012.123).abs() < 0.001);
352 assert!((coords.1 - 1247892.456).abs() < 0.001);
353 assert!((coords.2.unwrap() - 432.789).abs() < 0.001);
354 }
355
356 #[test]
357 fn test_extract_point_coordinates_2d() {
358 let text = "IFCCARTESIANPOINT((100.5,200.5))";
359 let coords = extract_point_coordinates(text).unwrap();
360
361 assert_eq!(coords.0, 100.5);
362 assert_eq!(coords.1, 200.5);
363 assert!(coords.2.is_none());
364 }
365
366 #[test]
367 fn test_scan_model_bounds() {
368 let ifc_content = r#"
369ISO-10303-21;
370HEADER;
371FILE_DESCRIPTION((''),'2;1');
372ENDSEC;
373DATA;
374#1=IFCCARTESIANPOINT((2679012.0,1247892.0,432.0));
375#2=IFCCARTESIANPOINT((2679112.0,1247992.0,442.0));
376#3=IFCWALL('guid',$,$,$,$,$,$,$);
377ENDSEC;
378END-ISO-10303-21;
379"#;
380
381 let bounds = scan_model_bounds(ifc_content);
382
383 assert!(bounds.is_valid());
384 assert!(bounds.has_large_coordinates());
385 assert_eq!(bounds.sample_count, 2);
386
387 let centroid = bounds.centroid();
388 assert!((centroid.0 - 2679062.0).abs() < 0.001);
389 assert!((centroid.1 - 1247942.0).abs() < 0.001);
390 }
391
392 #[test]
393 fn test_scan_model_bounds_small_model() {
394 let ifc_content = r#"
395ISO-10303-21;
396DATA;
397#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
398#2=IFCCARTESIANPOINT((10.0,10.0,5.0));
399ENDSEC;
400END-ISO-10303-21;
401"#;
402
403 let bounds = scan_model_bounds(ifc_content);
404
405 assert!(bounds.is_valid());
406 assert!(!bounds.has_large_coordinates());
407
408 let offset = bounds.rtc_offset();
409 assert_eq!(offset.0, 0.0); }
411
412 #[test]
413 fn test_precision_preserved_with_rtc() {
414 let x1 = 2679012.123456_f64;
418 let x2 = 2679012.223456_f64; let expected_diff = 0.1;
420
421 let x1_f32_direct = x1 as f32;
423 let x2_f32_direct = x2 as f32;
424 let diff_direct = x2_f32_direct - x1_f32_direct;
425 let error_direct = (diff_direct as f64 - expected_diff).abs();
426
427 let centroid = (x1 + x2) / 2.0;
429 let x1_shifted = (x1 - centroid) as f32;
430 let x2_shifted = (x2 - centroid) as f32;
431 let diff_rtc = x2_shifted - x1_shifted;
432 let error_rtc = (diff_rtc as f64 - expected_diff).abs();
433
434 println!("Without RTC: diff={}, error={}", diff_direct, error_direct);
435 println!("With RTC: diff={}, error={}", diff_rtc, error_rtc);
436
437 assert!(
441 error_rtc < error_direct * 0.1 || error_rtc < 0.0001,
442 "RTC should significantly improve precision"
443 );
444 }
445}