use std::sync::Arc;
use ifc_geometry::Transform;
use ifc_model::{Entity, EntityId, Model};
use ifc_properties::{
exact_predefined_sets, exact_schema, exact_unit, ExactPredefinedSet, ExactPropertyError,
ExactSource, ExactValue, SchemaVersion,
};
use ifc_schema::Schema;
use super::WindowOperationError as E;
use super::{
layout, WindowOperationError, WindowPanelOperation, WindowPanelPosition, WindowPartitioning,
};
use crate::operation;
pub(super) struct WindowInputs {
pub(super) partitioning: WindowPartitioning,
pub(super) partitioning_source: ExactSource,
pub(super) panel_source: ExactSource,
pub(super) overall_width: f64,
pub(super) overall_height: f64,
pub(super) world: Transform,
pub(super) panels: Vec<Panel>,
pub(super) lining: Option<Lining>,
}
#[derive(Debug, Clone)]
pub(super) struct Panel {
pub(super) set: EntityId,
pub(super) position: WindowPanelPosition,
pub(super) operation: WindowPanelOperation,
pub(super) frame_depth: Option<f64>,
pub(super) frame_thickness: Option<f64>,
}
#[derive(Debug, Clone)]
pub(super) struct Lining {
pub(super) source: ExactSource,
pub(super) set: EntityId,
pub(super) first_mullion: Option<f64>,
pub(super) second_mullion: Option<f64>,
pub(super) first_transom: Option<f64>,
pub(super) second_transom: Option<f64>,
}
pub(super) fn read_window(
model: &Model,
window: EntityId,
) -> Result<WindowInputs, WindowOperationError> {
let version = exact_schema(model)?;
let schema = ifc_schema::for_version(version).expect("exact releases are bundled");
let entity = model
.get(window)
.ok_or(ExactPropertyError::MissingReference {
from: window,
to: window,
})?;
let sets = exact_predefined_sets(model, window, "IfcWindowPanelProperties")?;
if !schema.is_a(&entity.type_name, "IFCWINDOW") {
return Err(E::NotAWindow {
entity: window,
type_name: entity.type_name.clone(),
});
}
let (written, partitioning_source) = partitioning(model, schema, version, window, entity)?;
let partitioning = layout::classify_partitioning(&written)?;
let length = |name, missing| {
operation::positive_length(model, schema, window, entity, name)
.map_err(E::read)?
.ok_or(missing)
};
let overall_width = length("OverallWidth", E::MissingOverallWidth { window })?;
let overall_height = length("OverallHeight", E::MissingOverallHeight { window })?;
let (panel_source, panels) =
operation::governing(&sets).ok_or(E::NoPanelProperties { window })?;
let panels = panels
.iter()
.map(|set| panel(model, set))
.collect::<Result<_, _>>()?;
let lining = if partitioning == WindowPartitioning::SinglePanel {
None
} else {
lining(model, window)?
};
let world = operation::world_transform(model, window).map_err(E::read)?;
Ok(WindowInputs {
partitioning,
partitioning_source,
panel_source,
overall_width,
overall_height,
world,
panels,
lining,
})
}
fn partitioning(
model: &Model,
schema: &'static Schema,
version: SchemaVersion,
window: EntityId,
entity: &Entity,
) -> Result<(Arc<str>, ExactSource), WindowOperationError> {
let occurrence =
operation::enumeration(schema, window, entity, "PartitioningType").map_err(E::read)?;
let typed = match operation::type_object(model, schema, window).map_err(E::read)? {
Some(type_id) => {
let type_entity = model.get(type_id).expect("validated reference");
let (expected, attribute) = match version {
SchemaVersion::Ifc2x3 => ("IFCWINDOWSTYLE", "OperationType"),
SchemaVersion::Ifc4 | SchemaVersion::Ifc4x3 => {
("IFCWINDOWTYPE", "PartitioningType")
}
other => {
return Err(ExactPropertyError::UnsupportedSchema {
schema: format!("{other:?}"),
}
.into())
}
};
if !schema.is_a(&type_entity.type_name, expected) {
return Err(E::UnsupportedTypeObject {
type_object: type_id,
type_name: type_entity.type_name.clone(),
});
}
let value = operation::enumeration(schema, type_id, type_entity, attribute)
.map_err(E::read)?
.ok_or(E::MalformedAttribute {
entity: type_id,
attribute,
})?;
Some((type_id, value))
}
None => None,
};
match (occurrence, typed) {
(Some(occurrence), Some((type_object, type_value))) if occurrence != type_value => {
Err(E::ConflictingPartitioningType {
occurrence,
type_object,
type_value,
})
}
(Some(occurrence), _) => Ok((occurrence, ExactSource::Occurrence)),
(None, Some((type_object, value))) => Ok((value, ExactSource::Type(type_object))),
(None, None) => Err(E::MissingPartitioningType { window }),
}
}
fn panel(model: &Model, set: &ExactPredefinedSet) -> Result<Panel, WindowOperationError> {
let malformed = |attribute| E::MalformedAttribute {
entity: set.set_id,
attribute,
};
let text = |name: &'static str| match set.attribute(name).map(|a| &a.value) {
Some(ExactValue::Enum(value)) => Ok(value.clone()),
_ => Err(malformed(name)),
};
let position = match &*text("PanelPosition")? {
"LEFT" => WindowPanelPosition::Left,
"MIDDLE" => WindowPanelPosition::Middle,
"RIGHT" => WindowPanelPosition::Right,
"BOTTOM" => WindowPanelPosition::Bottom,
"TOP" => WindowPanelPosition::Top,
"NOTDEFINED" => WindowPanelPosition::NotDefined,
_ => return Err(malformed("PanelPosition")),
};
let operation = layout::classify_panel(set.set_id, &text("OperationType")?)?;
let length = |name: &'static str| -> Result<Option<f64>, WindowOperationError> {
let Some(property) = set.attribute(name) else {
return Err(malformed(name));
};
match property.value {
ExactValue::Null => Ok(None),
ExactValue::Real(value) if value.is_finite() && value > 0.0 => {
let unit = property.value_type.as_deref().ok_or(malformed(name))?;
Ok(Some(
value * exact_unit(model, unit, property.unit_id)?.scale,
))
}
_ => Err(malformed(name)),
}
};
Ok(Panel {
set: set.set_id,
position,
operation,
frame_depth: length("FrameDepth")?,
frame_thickness: length("FrameThickness")?,
})
}
fn lining(model: &Model, window: EntityId) -> Result<Option<Lining>, WindowOperationError> {
let sets = exact_predefined_sets(model, window, "IfcWindowLiningProperties")?;
let Some((source, governing)) = operation::governing(&sets) else {
return Ok(None);
};
let [set] = governing[..] else {
return Err(E::LiningCount {
found: governing.len(),
});
};
let ratio = |name: &'static str| match set.attribute(name).map(|a| &a.value) {
Some(ExactValue::Null) => Ok(None),
Some(ExactValue::Real(value)) => Ok(Some(*value)),
_ => Err(E::MalformedAttribute {
entity: set.set_id,
attribute: name,
}),
};
Ok(Some(Lining {
source,
set: set.set_id,
first_mullion: ratio("FirstMullionOffset")?,
second_mullion: ratio("SecondMullionOffset")?,
first_transom: ratio("FirstTransomOffset")?,
second_transom: ratio("SecondTransomOffset")?,
}))
}