use ifc_model::{Entity, EntityId, Model, Value};
use ifc_schema::{for_version, Attribute, Schema, SchemaVersion};
use crate::{MaterialError, MaterialResult};
pub(crate) const IFC4_LAYOUT: &[(&str, &[&str])] = &[
("IFCMATERIAL", &["Name", "Description", "Category"]),
(
"IFCMATERIALCLASSIFICATIONRELATIONSHIP",
&["MaterialClassifications", "ClassifiedMaterial"],
),
(
"IFCMATERIALCONSTITUENT",
&["Name", "Description", "Material", "Fraction", "Category"],
),
(
"IFCMATERIALCONSTITUENTSET",
&["Name", "Description", "MaterialConstituents"],
),
(
"IFCMATERIALDEFINITIONREPRESENTATION",
&[
"Name",
"Description",
"Representations",
"RepresentedMaterial",
],
),
("IFCMATERIALLAYER", LAYER),
(
"IFCMATERIALLAYERSET",
&["MaterialLayers", "LayerSetName", "Description"],
),
(
"IFCMATERIALLAYERSETUSAGE",
&[
"ForLayerSet",
"LayerSetDirection",
"DirectionSense",
"OffsetFromReferenceLine",
"ReferenceExtent",
],
),
(
"IFCMATERIALLAYERWITHOFFSETS",
&[
"Material",
"LayerThickness",
"IsVentilated",
"Name",
"Description",
"Category",
"Priority",
"OffsetDirection",
"OffsetValues",
],
),
("IFCMATERIALLIST", &["Materials"]),
("IFCMATERIALPROFILE", PROFILE),
(
"IFCMATERIALPROFILESET",
&[
"Name",
"Description",
"MaterialProfiles",
"CompositeProfile",
],
),
("IFCMATERIALPROFILESETUSAGE", PROFILE_USAGE),
(
"IFCMATERIALPROFILESETUSAGETAPERING",
&[
"ForProfileSet",
"CardinalPoint",
"ReferenceExtent",
"ForProfileEndSet",
"CardinalEndPoint",
],
),
(
"IFCMATERIALPROFILEWITHOFFSETS",
&[
"Name",
"Description",
"Material",
"Profile",
"Priority",
"Category",
"OffsetValues",
],
),
(
"IFCMATERIALPROPERTIES",
&["Name", "Description", "Properties", "Material"],
),
(
"IFCMATERIALRELATIONSHIP",
&[
"Name",
"Description",
"RelatingMaterial",
"RelatedMaterials",
"Expression",
],
),
("IFCRELASSOCIATESMATERIAL", ROOT_RELATION_MATERIAL),
("IFCRELDEFINESBYTYPE", ROOT_RELATION_TYPE),
];
const LAYER: &[&str] = &[
"Material",
"LayerThickness",
"IsVentilated",
"Name",
"Description",
"Category",
"Priority",
];
const PROFILE: &[&str] = &[
"Name",
"Description",
"Material",
"Profile",
"Priority",
"Category",
];
const PROFILE_USAGE: &[&str] = &["ForProfileSet", "CardinalPoint", "ReferenceExtent"];
const ROOT_RELATION_MATERIAL: &[&str] = &[
"GlobalId",
"OwnerHistory",
"Name",
"Description",
"RelatedObjects",
"RelatingMaterial",
];
const ROOT_RELATION_TYPE: &[&str] = &[
"GlobalId",
"OwnerHistory",
"Name",
"Description",
"RelatedObjects",
"RelatingType",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Release<'m> {
Bound(SchemaVersion),
Multiple(usize),
Unsupported(&'m str),
}
impl<'m> Release<'m> {
pub(crate) const LEGACY: Release<'static> = Release::Bound(SchemaVersion::Ifc4);
pub(crate) fn of(model: &'m Model) -> Self {
match model.header().schema.as_slice() {
[] => Release::LEGACY,
[token] => SchemaVersion::from_header_token(token)
.map_or(Self::Unsupported(token.as_str()), Self::Bound),
tokens => Self::Multiple(tokens.len()),
}
}
pub(crate) fn bound(self) -> MaterialResult<(SchemaVersion, &'static Schema)> {
match self {
Self::Bound(version) => Ok((
version,
for_version(version).expect("every SchemaVersion has a bundled table"),
)),
Self::Multiple(schemas) => Err(MaterialError::MultipleSchemas { schemas }),
Self::Unsupported(schema) => Err(MaterialError::UnsupportedSchema {
schema: schema.to_owned(),
}),
}
}
pub(crate) fn instantiates(self, entity: &str) -> bool {
self.bound()
.is_ok_and(|(_, schema)| instantiable(schema, entity))
}
pub(crate) fn require_entity(
self,
entity: &'static str,
id: Option<EntityId>,
) -> MaterialResult<(SchemaVersion, &'static Schema)> {
let (version, schema) = self.bound()?;
if instantiable(schema, entity) {
Ok((version, schema))
} else {
Err(MaterialError::EntityNotInSchema {
entity,
id,
schema: version,
})
}
}
pub(crate) fn attribute(
self,
entity: &'static str,
id: EntityId,
attribute: &'static str,
) -> MaterialResult<(usize, &'static Attribute)> {
debug_assert!(
in_ifc4_layout(entity, attribute),
"{entity}.{attribute} is read but missing from IFC4_LAYOUT"
);
let (version, schema) = self.require_entity(entity, Some(id))?;
position(schema, version, entity, attribute).ok_or(MaterialError::NotInSchema {
entity,
id,
attribute,
schema: version,
})
}
pub(crate) fn slot(
self,
entity: &'static str,
id: EntityId,
attribute: &'static str,
) -> MaterialResult<usize> {
self.attribute(entity, id, attribute).map(|(slot, _)| slot)
}
pub(crate) fn accepts(
self,
entity: &'static str,
attribute: &'static str,
candidate: &str,
) -> MaterialResult<(bool, &'static str)> {
let (_, schema) = self.bound()?;
let declared = self.declared(entity, attribute)?.type_name.as_str();
Ok((
instantiable(schema, candidate) && schema.accepts_type(declared, candidate),
declared,
))
}
pub(crate) fn declared(
self,
entity: &'static str,
attribute: &'static str,
) -> MaterialResult<&'static Attribute> {
let (version, schema) = self.require_entity(entity, None)?;
position(schema, version, entity, attribute)
.map(|(_, declared)| declared)
.ok_or(MaterialError::AuthoringNotInSchema {
entity,
attribute,
schema: version,
})
}
pub(crate) fn record(
self,
entity: &'static str,
values: Vec<(&'static str, Value)>,
) -> MaterialResult<Entity> {
let (version, schema) = self.require_entity(entity, None)?;
let declared = schema.attributes(entity);
let mut slots = vec![None; declared.len()];
for (attribute, value) in values {
debug_assert!(
in_ifc4_layout(entity, attribute),
"{entity}.{attribute} is written but missing from IFC4_LAYOUT"
);
match position(schema, version, entity, attribute) {
Some((slot, _)) => slots[slot] = Some(value),
None if value == Value::Null => {}
None => {
return Err(MaterialError::AuthoringNotInSchema {
entity,
attribute,
schema: version,
})
}
}
}
let mut attributes = Vec::with_capacity(slots.len());
for (slot, value) in slots.into_iter().enumerate() {
let declaration = declared[slot];
match value.unwrap_or(Value::Null) {
Value::Null if !declaration.optional => {
return Err(MaterialError::AuthoringRequired {
entity,
attribute: declaration.name.as_str(),
schema: version,
})
}
value => attributes.push(value),
}
}
Ok(Entity::new(entity, attributes))
}
}
fn instantiable(schema: &Schema, entity: &str) -> bool {
schema
.entity(entity)
.is_some_and(|declaration| !declaration.abstract_)
}
fn position(
schema: &'static Schema,
version: SchemaVersion,
entity: &str,
attribute: &'static str,
) -> Option<(usize, &'static Attribute)> {
let name = release_name(version, entity, attribute);
schema
.attributes(entity)
.into_iter()
.enumerate()
.find(|(_, declared)| declared.name.eq_ignore_ascii_case(name))
}
fn in_ifc4_layout(entity: &str, attribute: &str) -> bool {
IFC4_LAYOUT.iter().any(|(declared, attributes)| {
declared.eq_ignore_ascii_case(entity) && attributes.contains(&attribute)
})
}
pub(crate) fn release_name(
release: SchemaVersion,
entity: &str,
attribute: &'static str,
) -> &'static str {
match (release, attribute) {
(SchemaVersion::Ifc4x3, "Expression")
if entity.eq_ignore_ascii_case("IFCMATERIALRELATIONSHIP") =>
{
"MaterialExpression"
}
_ => attribute,
}
}
pub(crate) fn is_type_object(release: Release<'_>, entity: &str) -> MaterialResult<bool> {
let (_, schema) = release.bound()?;
Ok(instantiable(schema, entity) && schema.is_a(entity, "IFCTYPEOBJECT"))
}
pub fn material_schema(model: &Model) -> MaterialResult<SchemaVersion> {
Release::of(model).bound().map(|(version, _)| version)
}
#[cfg(test)]
mod tests;