mod composites;
mod relationships;
pub use composites::{
create_constituent, create_constituent_set, create_profile, create_profile_set,
create_profile_set_usage, create_profile_set_usage_tapering, create_profile_with_offsets,
ConstituentDraft, ProfileDraft,
};
pub use relationships::{
create_material_classification_relationship, create_material_definition_representation,
create_material_properties, create_material_relationship,
};
use std::collections::HashSet;
use ifc_model::{Edit, EntityId, Model, Transaction, Value};
use crate::release::Release;
use crate::{DirectionSense, LayerSetDirection, LogicalValue, MaterialError, MaterialResult};
#[derive(Debug, Clone, Copy)]
pub struct MaterialDraft<'a> {
pub name: &'a str,
pub description: Option<&'a str>,
pub category: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct LayerDraft<'a> {
pub material: Option<EntityId>,
pub thickness: f64,
pub is_ventilated: Option<LogicalValue>,
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub category: Option<&'a str>,
pub priority: Option<i64>,
}
#[derive(Debug, Clone, Copy)]
pub struct LayerSetDraft<'a> {
pub layers: &'a [EntityId],
pub name: Option<&'a str>,
pub description: Option<&'a str>,
}
#[derive(Debug, Clone, Copy)]
pub struct MaterialAssignmentDraft<'a> {
pub global_id: &'a str,
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub related_objects: &'a [EntityId],
pub relating_material: EntityId,
}
pub fn create_material(
tx: &mut Transaction,
model: &Model,
draft: MaterialDraft<'_>,
) -> MaterialResult<EntityId> {
let record = Release::of(model).record(
"IFCMATERIAL",
vec![
("Name", text(draft.name)),
("Description", optional_text(draft.description)),
("Category", optional_text(draft.category)),
],
)?;
Ok(tx.create(record))
}
fn layer_values(
release: Release<'_>,
entity: &'static str,
draft: &LayerDraft<'_>,
) -> MaterialResult<Vec<(&'static str, Value)>> {
let declared = release.declared(entity, "LayerThickness")?;
let positive = declared
.type_name
.eq_ignore_ascii_case("IfcPositiveLengthMeasure");
if !draft.thickness.is_finite() || draft.thickness < 0.0 {
return Err(invalid(
entity,
"LayerThickness",
"expected a finite non-negative length",
));
}
if positive && draft.thickness == 0.0 {
return Err(invalid(
entity,
"LayerThickness",
"expected a positive length (IfcPositiveLengthMeasure)",
));
}
if let Some(priority) = draft.priority.filter(|value| !(0..=100).contains(value)) {
return Err(invalid(entity, "Priority", priority.to_string()));
}
Ok(vec![
("Material", draft.material.map_or(Value::Null, Value::Ref)),
("LayerThickness", Value::Real(draft.thickness)),
(
"IsVentilated",
draft.is_ventilated.map_or(Value::Null, logical),
),
("Name", optional_text(draft.name)),
("Description", optional_text(draft.description)),
("Category", optional_text(draft.category)),
(
"Priority",
draft.priority.map_or(Value::Null, Value::Integer),
),
])
}
pub fn create_layer(
tx: &mut Transaction,
model: &Model,
draft: LayerDraft<'_>,
) -> MaterialResult<EntityId> {
const ENTITY: &str = "IFCMATERIALLAYER";
let release = Release::of(model);
let values = layer_values(release, ENTITY, &draft)?;
if let Some(material) = draft.material {
require_type(tx, model, release, material, &["IFCMATERIAL"])?;
}
Ok(tx.create(release.record(ENTITY, values)?))
}
pub fn create_layer_set(
tx: &mut Transaction,
model: &Model,
draft: LayerSetDraft<'_>,
) -> MaterialResult<EntityId> {
const ENTITY: &str = "IFCMATERIALLAYERSET";
let release = Release::of(model);
release.require_entity(ENTITY, None)?;
if draft.layers.is_empty() {
return Err(invalid(
ENTITY,
"MaterialLayers",
"expected at least one layer",
));
}
for &layer in draft.layers {
require_type(
tx,
model,
release,
layer,
&["IFCMATERIALLAYER", "IFCMATERIALLAYERWITHOFFSETS"],
)?;
}
let record = release.record(
ENTITY,
vec![
("MaterialLayers", refs(draft.layers)),
("LayerSetName", optional_text(draft.name)),
("Description", optional_text(draft.description)),
],
)?;
Ok(tx.create(record))
}
pub fn associate_material(
tx: &mut Transaction,
model: &Model,
draft: MaterialAssignmentDraft<'_>,
) -> MaterialResult<EntityId> {
associate(tx, model, draft, None)
}
pub fn associate_material_with_owner_history(
tx: &mut Transaction,
model: &Model,
draft: MaterialAssignmentDraft<'_>,
owner_history: EntityId,
) -> MaterialResult<EntityId> {
associate(tx, model, draft, Some(owner_history))
}
fn associate(
tx: &mut Transaction,
model: &Model,
draft: MaterialAssignmentDraft<'_>,
owner_history: Option<EntityId>,
) -> MaterialResult<EntityId> {
const ENTITY: &str = "IFCRELASSOCIATESMATERIAL";
let release = Release::of(model);
release.require_entity(ENTITY, None)?;
if ifc_model::guid::Guid::parse(draft.global_id).is_none() {
return Err(invalid(ENTITY, "GlobalId", "expected IFC compressed GUID"));
}
if draft.related_objects.is_empty() {
return Err(invalid(
ENTITY,
"RelatedObjects",
"expected at least one object",
));
}
let mut unique = HashSet::new();
for &object in draft.related_objects {
if !unique.insert(object) {
return Err(invalid(
ENTITY,
"RelatedObjects",
"duplicate object reference",
));
}
require_exists(tx, model, object)?;
}
require_accepts(
tx,
model,
release,
ENTITY,
"RelatingMaterial",
draft.relating_material,
)?;
if let Some(owner_history) = owner_history {
require_type(tx, model, release, owner_history, &["IFCOWNERHISTORY"])?;
}
let record = release.record(
ENTITY,
vec![
("GlobalId", text(draft.global_id)),
(
"OwnerHistory",
owner_history.map_or(Value::Null, Value::Ref),
),
("Name", optional_text(draft.name)),
("Description", optional_text(draft.description)),
("RelatedObjects", refs(draft.related_objects)),
("RelatingMaterial", Value::Ref(draft.relating_material)),
],
)?;
Ok(tx.create(record))
}
pub fn create_material_list(
tx: &mut Transaction,
model: &Model,
materials: &[EntityId],
) -> MaterialResult<EntityId> {
const ENTITY: &str = "IFCMATERIALLIST";
let release = Release::of(model);
release.require_entity(ENTITY, None)?;
if materials.is_empty() {
return Err(invalid(
ENTITY,
"Materials",
"expected at least one material",
));
}
for &material in materials {
require_type(tx, model, release, material, &["IFCMATERIAL"])?;
}
Ok(tx.create(release.record(ENTITY, vec![("Materials", refs(materials))])?))
}
pub fn create_layer_set_usage(
tx: &mut Transaction,
model: &Model,
for_layer_set: EntityId,
direction: LayerSetDirection,
sense: DirectionSense,
offset_from_reference_line: f64,
reference_extent: Option<f64>,
) -> MaterialResult<EntityId> {
const ENTITY: &str = "IFCMATERIALLAYERSETUSAGE";
let release = Release::of(model);
release.require_entity(ENTITY, None)?;
require_type(tx, model, release, for_layer_set, &["IFCMATERIALLAYERSET"])?;
if !offset_from_reference_line.is_finite() {
return Err(invalid(
ENTITY,
"OffsetFromReferenceLine",
"expected a finite length",
));
}
let record = release.record(
ENTITY,
vec![
("ForLayerSet", Value::Ref(for_layer_set)),
(
"LayerSetDirection",
Value::Enum(direction.as_token().into()),
),
("DirectionSense", Value::Enum(sense.as_token().into())),
(
"OffsetFromReferenceLine",
Value::Real(offset_from_reference_line),
),
(
"ReferenceExtent",
reference_extent.map_or(Value::Null, Value::Real),
),
],
)?;
Ok(tx.create(record))
}
pub fn create_layer_with_offsets(
tx: &mut Transaction,
model: &Model,
draft: LayerDraft<'_>,
offset_direction: LayerSetDirection,
offset_values: [f64; 2],
) -> MaterialResult<EntityId> {
const ENTITY: &str = "IFCMATERIALLAYERWITHOFFSETS";
let release = Release::of(model);
let mut values = layer_values(release, ENTITY, &draft)?;
if offset_values.iter().any(|value| !value.is_finite()) {
return Err(invalid(ENTITY, "OffsetValues", "expected finite lengths"));
}
if let Some(material) = draft.material {
require_type(tx, model, release, material, &["IFCMATERIAL"])?;
}
values.push((
"OffsetDirection",
Value::Enum(offset_direction.as_token().into()),
));
values.push(("OffsetValues", reals(&offset_values)));
Ok(tx.create(release.record(ENTITY, values)?))
}
pub(crate) fn require_exists(tx: &Transaction, model: &Model, id: EntityId) -> MaterialResult<()> {
if type_name(tx, model, id).is_some() {
Ok(())
} else {
Err(MaterialError::UnknownEntity { id })
}
}
pub(crate) fn require_type(
tx: &Transaction,
model: &Model,
release: Release<'_>,
id: EntityId,
expected: &[&'static str],
) -> MaterialResult<()> {
let actual = type_name(tx, model, id).ok_or(MaterialError::UnknownEntity { id })?;
if expected
.iter()
.any(|kind| actual.eq_ignore_ascii_case(kind) && release.instantiates(kind))
{
Ok(())
} else {
Err(MaterialError::AuthoringReferenceType {
target: id,
expected: expected[0],
actual: actual.to_owned(),
})
}
}
pub(crate) fn require_accepts(
tx: &Transaction,
model: &Model,
release: Release<'_>,
entity: &'static str,
attribute: &'static str,
id: EntityId,
) -> MaterialResult<()> {
let actual = type_name(tx, model, id).ok_or(MaterialError::UnknownEntity { id })?;
let (accepted, declared) = release.accepts(entity, attribute, actual)?;
if accepted {
Ok(())
} else {
Err(MaterialError::AuthoringReferenceType {
target: id,
expected: declared,
actual: actual.to_owned(),
})
}
}
fn type_name<'a>(tx: &'a Transaction, model: &'a Model, id: EntityId) -> Option<&'a str> {
for edit in tx.edits().iter().rev() {
match edit {
Edit::Create {
id: candidate,
entity,
} if *candidate == id => return Some(&entity.type_name),
Edit::Retype {
id: candidate,
type_name,
} if *candidate == id => return Some(type_name),
Edit::Remove { id: candidate } if *candidate == id => return None,
_ => {}
}
}
model.get(id).map(|entity| entity.type_name.as_ref())
}
pub(crate) fn invalid(
entity: &'static str,
attribute: &'static str,
value: impl Into<String>,
) -> MaterialError {
MaterialError::AuthoringInvalid {
entity,
attribute,
value: value.into(),
}
}
fn text(value: &str) -> Value {
Value::Text(value.into())
}
pub(crate) fn optional_text(value: Option<&str>) -> Value {
value.map_or(Value::Null, text)
}
pub(crate) fn refs(ids: &[EntityId]) -> Value {
Value::List(ids.iter().copied().map(Value::Ref).collect())
}
pub(crate) fn reals(values: &[f64]) -> Value {
Value::List(values.iter().copied().map(Value::Real).collect())
}
fn logical(value: LogicalValue) -> Value {
match value {
LogicalValue::False => Value::Bool(false),
LogicalValue::True => Value::Bool(true),
LogicalValue::Unknown => Value::LogicalUnknown,
}
}