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, length_unit_scale: f64) -> bool {
if !self.is_valid() {
return false;
}
[
self.min_x,
self.min_y,
self.min_z,
self.max_x,
self.max_y,
self.max_z,
]
.iter()
.any(|v| (v * length_unit_scale).abs() > crate::limits::LARGE_COORD_THRESHOLD_METERS)
}
#[inline]
pub fn rtc_offset(&self, length_unit_scale: f64) -> Option<crate::RtcVerdict> {
if !self.is_valid() {
return None;
}
Some(if self.has_large_coordinates(length_unit_scale) {
let (x, y, z) = self.centroid();
let s = length_unit_scale;
crate::RtcVerdict::Large { anchor: (x * s, y * s, z * s) }
} else {
crate::RtcVerdict::Small
})
}
}
impl Default for ModelBounds {
fn default() -> Self {
Self::new()
}
}
pub fn scan_model_bounds<T>(content: &T) -> ModelBounds
where
T: AsRef<[u8]> + ?Sized,
{
let content = content.as_ref();
let mut bounds = ModelBounds::new();
let mut scanner = EntityScanner::new(content);
while let Some((_id, type_name, start, end)) = scanner.next_entity() {
if !crate::keyword_eq(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<T>(bytes: &T) -> Option<(f64, f64, Option<f64>)>
where
T: AsRef<[u8]> + ?Sized,
{
let bytes = bytes.as_ref();
let text = std::str::from_utf8(bytes).ok()?;
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<T>(content: &T) -> ModelBounds
where
T: AsRef<[u8]> + ?Sized,
{
let content = content.as_ref();
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 crate::keyword_eq(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);
}
}
}
scanner = EntityScanner::new(content);
while let Some((id, type_name, start, end)) = scanner.next_entity() {
if crate::keyword_eq(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<T>(bytes: &T) -> Option<u32>
where
T: AsRef<[u8]> + ?Sized,
{
let bytes = bytes.as_ref();
let text = std::str::from_utf8(bytes).ok()?;
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)]
#[path = "model_bounds_tests.rs"]
mod tests;