mod resolve;
use std::{fmt, sync::Arc};
use ifc_model::{EntityId, Model};
use ifc_schema::SchemaVersion;
use super::measure::measure_unit;
use super::refs::{ref_at, refs_at};
use super::release::{validate_model, Release};
use super::value::select_accepts_entity;
use super::ExactPropertyError;
use resolve::{declared_unit_type, Resolver};
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct ExactUnit {
pub unit: Option<EntityId>,
pub from_project: bool,
pub dimensions: [i32; 7],
pub scale: f64,
pub offset: f64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExactUnitError {
Structure(ExactPropertyError),
MeasureNotInSchema {
measure_type: Arc<str>,
schema: SchemaVersion,
},
NotAMeasure {
measure_type: Arc<str>,
},
UnmappedMeasureType {
measure_type: Arc<str>,
},
UnexpectedUnit {
unit: EntityId,
},
NoProject,
MultipleProjects {
first: EntityId,
second: EntityId,
},
NoProjectUnit {
unit_type: Arc<str>,
},
DuplicateProjectUnit {
unit_type: Arc<str>,
first: EntityId,
second: EntityId,
},
UnsupportedUnit {
unit: EntityId,
type_name: Arc<str>,
},
MalformedUnit {
entity: EntityId,
attribute: &'static str,
},
UnknownPrefix {
unit: EntityId,
prefix: Arc<str>,
},
UnknownUnitName {
unit: EntityId,
name: Arc<str>,
},
UnitTypeMismatch {
unit: EntityId,
expected: Arc<str>,
found: Arc<str>,
},
DimensionMismatch {
unit: EntityId,
expected: [i32; 7],
found: [i32; 7],
},
UnsupportedOffset {
unit: EntityId,
},
CyclicConversion {
cycle: Vec<EntityId>,
},
ConversionChainTooDeep {
unit: EntityId,
max_depth: usize,
},
}
impl From<ExactPropertyError> for ExactUnitError {
fn from(error: ExactPropertyError) -> Self {
Self::Structure(error)
}
}
impl fmt::Display for ExactUnitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "exact IFC unit resolution failed: {self:?}")
}
}
impl std::error::Error for ExactUnitError {}
pub fn exact_unit(
model: &Model,
measure_type: &str,
explicit_unit: Option<EntityId>,
) -> Result<ExactUnit, ExactUnitError> {
let release = validate_model(model)?;
let target = measure_unit(release, measure_type)?;
let Some(expected) = target.unit_type().cloned() else {
if let Some(unit) = explicit_unit {
return Err(ExactUnitError::UnexpectedUnit { unit });
}
return Ok(ExactUnit {
unit: None,
from_project: false,
dimensions: [0; 7],
scale: 1.0,
offset: 0.0,
});
};
let (unit, from_project) = match explicit_unit {
Some(unit) => (unit, false),
None => (project_unit(model, release, &expected)?, true),
};
let resolved = Resolver::new(model, release).resolve(unit)?;
if !resolved.unit_type.eq_ignore_ascii_case(&expected) {
return Err(ExactUnitError::UnitTypeMismatch {
unit,
expected,
found: resolved.unit_type,
});
}
Ok(ExactUnit {
unit: Some(unit),
from_project,
dimensions: resolved.dimensions,
scale: resolved.scale,
offset: resolved.offset,
})
}
fn project_unit(
model: &Model,
release: Release,
unit_type: &Arc<str>,
) -> Result<EntityId, ExactUnitError> {
let project = match model.ids_of_type("IFCPROJECT") {
[] => return Err(ExactUnitError::NoProject),
[project] => *project,
[first, second, ..] => {
return Err(ExactUnitError::MultipleProjects {
first: *first,
second: *second,
})
}
};
let entity = model.get(project).expect("type index is current");
release.require_exact_slots(project, entity)?;
let slot = release
.schema
.attribute_names("IFCPROJECT")
.iter()
.position(|name| name.eq_ignore_ascii_case("UnitsInContext"))
.expect("IfcProject declares UnitsInContext in every bundled release");
let no_unit = || ExactUnitError::NoProjectUnit {
unit_type: unit_type.clone(),
};
if matches!(entity.attributes.get(slot), Some(ifc_model::Value::Null)) {
return Err(no_unit());
}
let assignment = ref_at(project, entity.attributes.get(slot), "UnitsInContext")?;
let assignment_entity = model
.get(assignment)
.ok_or(ExactPropertyError::MissingReference {
from: project,
to: assignment,
})?;
release.require_exact_slots(assignment, assignment_entity)?;
if !assignment_entity.is_type("IFCUNITASSIGNMENT") {
return Err(ExactUnitError::UnsupportedUnit {
unit: assignment,
type_name: assignment_entity.type_name.clone(),
});
}
let units = refs_at(assignment, assignment_entity.attributes.first(), "Units")?;
let mut found = None;
for unit in units {
let unit_entity = model
.get(unit)
.ok_or(ExactPropertyError::MissingReference {
from: assignment,
to: unit,
})?;
release.require_exact_slots(unit, unit_entity)?;
if !select_accepts_entity(release.schema, "IFCUNIT", &unit_entity.type_name) {
return Err(ExactUnitError::UnsupportedUnit {
unit,
type_name: unit_entity.type_name.clone(),
});
}
let declared = declared_unit_type(release, unit, unit_entity)?;
if declared
.as_deref()
.is_some_and(|declared| declared.eq_ignore_ascii_case(unit_type))
{
if let Some(first) = found.replace(unit) {
return Err(ExactUnitError::DuplicateProjectUnit {
unit_type: unit_type.clone(),
first,
second: unit,
});
}
}
}
found.ok_or_else(no_unit)
}