use ifc_model::guid::Guid;
use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
use super::{invalid, SpatialAuthoringResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoundaryLevel {
Base,
First,
Second,
}
impl BoundaryLevel {
fn type_name(self) -> &'static str {
match self {
Self::Base => "IFCRELSPACEBOUNDARY",
Self::First => "IFCRELSPACEBOUNDARY1STLEVEL",
Self::Second => "IFCRELSPACEBOUNDARY2NDLEVEL",
}
}
fn arity(self) -> usize {
match self {
Self::Base => 9,
Self::First => 10,
Self::Second => 11,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct BoundaryDraft<'a> {
pub name: Option<&'a str>,
pub description: Option<&'a str>,
pub space: EntityId,
pub element: EntityId,
pub connection_geometry: Option<EntityId>,
pub physical_or_virtual: &'a str,
pub internal_or_external: &'a str,
pub parent: Option<EntityId>,
pub corresponding: Option<EntityId>,
}
const PHYS_OR_VIRT: &[&str] = &["PHYSICAL", "VIRTUAL", "NOTDEFINED"];
const INT_OR_EXT: &[&str] = &[
"INTERNAL",
"EXTERNAL",
"EXTERNAL_EARTH",
"EXTERNAL_WATER",
"EXTERNAL_FIRE",
"NOTDEFINED",
];
fn type_name<'a>(tx: &'a Transaction, model: &'a Model, id: EntityId) -> Option<&'a str> {
tx.edits()
.iter()
.rev()
.find_map(|edit| match edit {
Edit::Create { id: staged, entity } if *staged == id => Some(entity.type_name.as_ref()),
_ => None,
})
.or_else(|| model.get(id).map(|entity| entity.type_name.as_ref()))
}
pub fn create_space_boundary(
tx: &mut Transaction,
model: &Model,
level: BoundaryLevel,
global_id: &str,
draft: BoundaryDraft<'_>,
) -> SpatialAuthoringResult<EntityId> {
let entity = level.type_name();
if Guid::parse(global_id).is_none() {
return Err(invalid(entity, "GlobalId", global_id));
}
let physical = draft.physical_or_virtual.to_ascii_uppercase();
if !PHYS_OR_VIRT.contains(&physical.as_str()) {
return Err(invalid(
entity,
"PhysicalOrVirtualBoundary",
draft.physical_or_virtual,
));
}
let internal = draft.internal_or_external.to_ascii_uppercase();
if !INT_OR_EXT.contains(&internal.as_str()) {
return Err(invalid(
entity,
"InternalOrExternalBoundary",
draft.internal_or_external,
));
}
let bounded_by = type_name(tx, model, draft.element)
.map(str::to_ascii_uppercase)
.unwrap_or_default();
let is_virtual = bounded_by == "IFCVIRTUALELEMENT";
let is_opening = bounded_by == "IFCOPENINGELEMENT";
let agrees = match physical.as_str() {
"PHYSICAL" => !is_virtual,
"VIRTUAL" => is_virtual || is_opening,
_ => true,
};
if !agrees {
return Err(invalid(
entity,
"PhysicalOrVirtualBoundary",
format!("{physical} does not agree with {bounded_by}"),
));
}
if draft.parent.is_some() && level == BoundaryLevel::Base {
return Err(invalid(
entity,
"ParentBoundary",
"not declared at this level",
));
}
if draft.corresponding.is_some() && level != BoundaryLevel::Second {
return Err(invalid(
entity,
"CorrespondingBoundary",
"not declared at this level",
));
}
let mut attributes = vec![Value::Null; level.arity()];
attributes[0] = Value::Text(global_id.into());
attributes[2] = draft.name.map_or(Value::Null, |t| Value::Text(t.into()));
attributes[3] = draft
.description
.map_or(Value::Null, |t| Value::Text(t.into()));
attributes[4] = Value::Ref(draft.space);
attributes[5] = Value::Ref(draft.element);
attributes[6] = draft.connection_geometry.map_or(Value::Null, Value::Ref);
attributes[7] = Value::Enum(physical.into());
attributes[8] = Value::Enum(internal.into());
if let Some(parent) = draft.parent {
attributes[9] = Value::Ref(parent);
}
if let Some(corresponding) = draft.corresponding {
attributes[10] = Value::Ref(corresponding);
}
Ok(tx.create(Entity::new(entity, attributes)))
}
const CONNECTION_TYPE: &[&str] = &["ATPATH", "ATSTART", "ATEND", "NOTDEFINED"];
pub fn connect_path_elements(
tx: &mut Transaction,
global_id: &str,
relating: EntityId,
related: EntityId,
priorities: (&[i64], &[i64]),
connection_types: (&str, &str),
) -> SpatialAuthoringResult<EntityId> {
const ENTITY: &str = "IFCRELCONNECTSPATHELEMENTS";
if Guid::parse(global_id).is_none() {
return Err(invalid(ENTITY, "GlobalId", global_id));
}
if relating == related {
return Err(invalid(ENTITY, "RelatedElement", "is the relating element"));
}
let (relating_priorities, related_priorities) = priorities;
for (values, attribute) in [
(relating_priorities, "RelatingPriorities"),
(related_priorities, "RelatedPriorities"),
] {
if let Some(out) = values.iter().find(|value| !(0..=100).contains(*value)) {
return Err(invalid(ENTITY, attribute, out.to_string()));
}
}
let (relating_type, related_type) = connection_types;
let relating_token = relating_type.to_ascii_uppercase();
let related_token = related_type.to_ascii_uppercase();
for (token, attribute) in [
(&relating_token, "RelatingConnectionType"),
(&related_token, "RelatedConnectionType"),
] {
if !CONNECTION_TYPE.contains(&token.as_str()) {
return Err(invalid(ENTITY, attribute, token.clone()));
}
}
let integers =
|values: &[i64]| Value::List(values.iter().copied().map(Value::Integer).collect());
let mut attributes = vec![Value::Null; 11];
attributes[0] = Value::Text(global_id.into());
attributes[5] = Value::Ref(relating);
attributes[6] = Value::Ref(related);
attributes[7] = integers(relating_priorities);
attributes[8] = integers(related_priorities);
attributes[9] = Value::Enum(related_token.into());
attributes[10] = Value::Enum(relating_token.into());
Ok(tx.create(Entity::new(ENTITY, attributes)))
}