use crate::EntityScanner;
use std::collections::HashSet;
#[derive(Debug, Clone)]
pub struct ModelBounds {
pub min_x: f64,
pub min_y: f64,
pub min_z: f64,
pub max_x: f64,
pub max_y: f64,
pub max_z: f64,
pub sample_count: usize,
}
impl ModelBounds {
pub fn new() -> Self {
Self {
min_x: f64::MAX,
min_y: f64::MAX,
min_z: f64::MAX,
max_x: f64::MIN,
max_y: f64::MIN,
max_z: f64::MIN,
sample_count: 0,
}
}
#[inline]
pub fn is_valid(&self) -> bool {
self.sample_count > 0
}
#[inline]
pub fn expand(&mut self, x: f64, y: f64, z: f64) {
self.min_x = self.min_x.min(x);
self.min_y = self.min_y.min(y);
self.min_z = self.min_z.min(z);
self.max_x = self.max_x.max(x);
self.max_y = self.max_y.max(y);
self.max_z = self.max_z.max(z);
self.sample_count += 1;
}
#[inline]
pub fn centroid(&self) -> (f64, f64, f64) {
if !self.is_valid() {
return (0.0, 0.0, 0.0);
}
(
(self.min_x + self.max_x) / 2.0,
(self.min_y + self.max_y) / 2.0,
(self.min_z + self.max_z) / 2.0,
)
}
#[inline]
pub fn has_large_coordinates(&self) -> bool {
const THRESHOLD: f64 = 10000.0; if !self.is_valid() {
return false;
}
self.min_x.abs() > THRESHOLD
|| self.min_y.abs() > THRESHOLD
|| self.max_x.abs() > THRESHOLD
|| self.max_y.abs() > THRESHOLD
|| self.min_z.abs() > THRESHOLD
|| self.max_z.abs() > THRESHOLD
}
#[inline]
pub fn rtc_offset(&self) -> (f64, f64, f64) {
if self.has_large_coordinates() {
self.centroid()
} else {
(0.0, 0.0, 0.0)
}
}
}
impl Default for ModelBounds {
fn default() -> Self {
Self::new()
}
}
pub fn scan_model_bounds(content: &str) -> ModelBounds {
let mut bounds = ModelBounds::new();
let mut scanner = EntityScanner::new(content);
while let Some((_id, type_name, start, end)) = scanner.next_entity() {
if type_name != "IFCCARTESIANPOINT" {
continue;
}
let entity_text = &content[start..end];
if let Some(coords) = extract_point_coordinates(entity_text) {
let x = coords.0;
let y = coords.1;
let z = coords.2.unwrap_or(0.0);
if x.is_finite() && y.is_finite() && z.is_finite() {
bounds.expand(x, y, z);
}
}
}
bounds
}
fn extract_point_coordinates(text: &str) -> Option<(f64, f64, Option<f64>)> {
let start = text.find("((")?;
let end = text.rfind("))")?;
if start >= end {
return None;
}
let coord_str = &text[start + 2..end];
let parts: Vec<&str> = coord_str.split(',').collect();
if parts.len() < 2 {
return None;
}
let x = parts[0].trim().parse::<f64>().ok()?;
let y = parts[1].trim().parse::<f64>().ok()?;
let z = if parts.len() > 2 {
parts[2].trim().parse::<f64>().ok()
} else {
None
};
Some((x, y, z))
}
pub fn scan_placement_bounds(content: &str) -> ModelBounds {
let mut bounds = ModelBounds::new();
let mut scanner = EntityScanner::new(content);
let mut placement_point_ids: HashSet<u32> = HashSet::new();
while let Some((_id, type_name, start, end)) = scanner.next_entity() {
if type_name == "IFCAXIS2PLACEMENT3D" {
let entity_text = &content[start..end];
if let Some(ref_id) = extract_first_reference(entity_text) {
placement_point_ids.insert(ref_id);
}
}
if type_name == "IFCSITE" {
}
if type_name == "IFCCARTESIANPOINT" {
continue;
}
}
scanner = EntityScanner::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if type_name == "IFCCARTESIANPOINT" {
let is_placement_point = placement_point_ids.contains(&id);
let entity_text = &content[start..end];
if let Some(coords) = extract_point_coordinates(entity_text) {
let x = coords.0;
let y = coords.1;
let z = coords.2.unwrap_or(0.0);
if !x.is_finite() || !y.is_finite() || !z.is_finite() {
continue;
}
if is_placement_point || x.abs() > 1000.0 || y.abs() > 1000.0 || z.abs() > 1000.0 {
bounds.expand(x, y, z);
}
}
}
}
if !bounds.is_valid() {
return scan_model_bounds(content);
}
bounds
}
fn extract_first_reference(text: &str) -> Option<u32> {
let start = text.find('(')?;
let rest = &text[start + 1..];
let hash_pos = rest.find('#')?;
let after_hash = &rest[hash_pos + 1..];
let end_pos = after_hash
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(after_hash.len());
if end_pos == 0 {
return None;
}
after_hash[..end_pos].parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bounds_creation() {
let bounds = ModelBounds::new();
assert!(!bounds.is_valid());
assert!(!bounds.has_large_coordinates());
}
#[test]
fn test_bounds_expand() {
let mut bounds = ModelBounds::new();
bounds.expand(100.0, 200.0, 50.0);
bounds.expand(150.0, 250.0, 75.0);
assert!(bounds.is_valid());
assert_eq!(bounds.min_x, 100.0);
assert_eq!(bounds.max_x, 150.0);
assert_eq!(bounds.min_y, 200.0);
assert_eq!(bounds.max_y, 250.0);
let centroid = bounds.centroid();
assert_eq!(centroid.0, 125.0);
assert_eq!(centroid.1, 225.0);
}
#[test]
fn test_large_coordinates_detection() {
let mut bounds = ModelBounds::new();
bounds.expand(2679012.0, 1247892.0, 432.0);
assert!(bounds.has_large_coordinates());
let offset = bounds.rtc_offset();
assert_eq!(offset.0, 2679012.0);
assert_eq!(offset.1, 1247892.0);
}
#[test]
fn test_small_coordinates_no_shift() {
let mut bounds = ModelBounds::new();
bounds.expand(0.0, 0.0, 0.0);
bounds.expand(100.0, 100.0, 10.0);
assert!(!bounds.has_large_coordinates());
let offset = bounds.rtc_offset();
assert_eq!(offset.0, 0.0);
assert_eq!(offset.1, 0.0);
assert_eq!(offset.2, 0.0);
}
#[test]
fn test_extract_point_coordinates_3d() {
let text = "IFCCARTESIANPOINT((2679012.123,1247892.456,432.789))";
let coords = extract_point_coordinates(text).unwrap();
assert!((coords.0 - 2679012.123).abs() < 0.001);
assert!((coords.1 - 1247892.456).abs() < 0.001);
assert!((coords.2.unwrap() - 432.789).abs() < 0.001);
}
#[test]
fn test_extract_point_coordinates_2d() {
let text = "IFCCARTESIANPOINT((100.5,200.5))";
let coords = extract_point_coordinates(text).unwrap();
assert_eq!(coords.0, 100.5);
assert_eq!(coords.1, 200.5);
assert!(coords.2.is_none());
}
#[test]
fn test_scan_model_bounds() {
let ifc_content = r#"
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
ENDSEC;
DATA;
#1=IFCCARTESIANPOINT((2679012.0,1247892.0,432.0));
#2=IFCCARTESIANPOINT((2679112.0,1247992.0,442.0));
#3=IFCWALL('guid',$,$,$,$,$,$,$);
ENDSEC;
END-ISO-10303-21;
"#;
let bounds = scan_model_bounds(ifc_content);
assert!(bounds.is_valid());
assert!(bounds.has_large_coordinates());
assert_eq!(bounds.sample_count, 2);
let centroid = bounds.centroid();
assert!((centroid.0 - 2679062.0).abs() < 0.001);
assert!((centroid.1 - 1247942.0).abs() < 0.001);
}
#[test]
fn test_scan_model_bounds_small_model() {
let ifc_content = r#"
ISO-10303-21;
DATA;
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((10.0,10.0,5.0));
ENDSEC;
END-ISO-10303-21;
"#;
let bounds = scan_model_bounds(ifc_content);
assert!(bounds.is_valid());
assert!(!bounds.has_large_coordinates());
let offset = bounds.rtc_offset();
assert_eq!(offset.0, 0.0); }
#[test]
fn test_precision_preserved_with_rtc() {
let x1 = 2679012.123456_f64;
let x2 = 2679012.223456_f64; let expected_diff = 0.1;
let x1_f32_direct = x1 as f32;
let x2_f32_direct = x2 as f32;
let diff_direct = x2_f32_direct - x1_f32_direct;
let error_direct = (diff_direct as f64 - expected_diff).abs();
let centroid = (x1 + x2) / 2.0;
let x1_shifted = (x1 - centroid) as f32;
let x2_shifted = (x2 - centroid) as f32;
let diff_rtc = x2_shifted - x1_shifted;
let error_rtc = (diff_rtc as f64 - expected_diff).abs();
println!("Without RTC: diff={}, error={}", diff_direct, error_direct);
println!("With RTC: diff={}, error={}", diff_rtc, error_rtc);
assert!(
error_rtc < error_direct * 0.1 || error_rtc < 0.0001,
"RTC should significantly improve precision"
);
}
}