ifc-systems 0.2.3

Distribution systems, ports, and connectivity between elements.
Documentation
//! Distribution element occurrences and the two grouping forms.
//!
//! # Why one writer per class and not one taking a type name
//!
//! `IfcFlowSegment`, `IfcFlowFitting` and their siblings share an
//! identical eight-slot layout inherited from `IfcDistributionElement`.
//! A single `create_distribution_element(type_name, ..)` would be less
//! code, but it would also accept `"IFCWALL"` or a typo, and the caller
//! would learn about it from a validator rather than the compiler. The
//! enum below keeps the set closed: every variant is a class the flow
//! reader in this crate already understands.
//!
//! # `IfcZone` and `IfcSpatialZone` are not the same thing
//!
//! Both group spaces, but `IfcZone` is an `IfcGroup` -- membership is
//! an objectified relationship, and it carries no placement. An
//! `IfcSpatialZone` is an `IfcSpatialElement`: it has its own
//! placement, representation and a `PredefinedType`, so it can occupy
//! geometry a zone cannot. Authoring the wrong one produces a file that
//! parses and then answers spatial queries incorrectly, which is why
//! they get separate constructors rather than a flag.

use ifc_model::{Entity, EntityId, Model, Transaction, Value};

use super::release::bind;
use crate::authoring::{invalid, SystemAuthoringResult};

/// `IfcDistributionElement` slots, from IFC4X3 ADD2.
///
/// All eight are inherited: `IfcRoot` contributes 0-3, `IfcObject`
/// contributes `ObjectType` at 4, `IfcProduct` contributes placement
/// and representation at 5-6, and `IfcElement` contributes `Tag` at 7.
/// None of the concrete subtypes in [`DistributionElementKind`] add an
/// attribute of their own, so the width is the same for every one.
pub(crate) mod distribution_slot {
    /// `GlobalId`.
    pub const GLOBAL_ID: usize = 0;
    /// `Name`.
    pub const NAME: usize = 2;
    /// `Description`.
    pub const DESCRIPTION: usize = 3;
    /// `ObjectPlacement`.
    pub const OBJECT_PLACEMENT: usize = 5;
    /// `Representation`.
    pub const REPRESENTATION: usize = 6;
    /// `Tag`.
    pub const TAG: usize = 7;
    /// Total attribute count.
    pub const WIDTH: usize = 8;
}

/// `IfcZone` slots. `IfcGroup` has no placement, so the list is short.
pub(crate) mod zone_slot {
    /// `GlobalId`.
    pub const GLOBAL_ID: usize = 0;
    /// `Name`.
    pub const NAME: usize = 2;
    /// `Description`.
    pub const DESCRIPTION: usize = 3;
    /// `LongName`, declared by `IfcZone` itself.
    pub const LONG_NAME: usize = 5;
    /// Total attribute count.
    pub const WIDTH: usize = 6;
}

/// `IfcSpatialZone` slots.
pub(crate) mod spatial_zone_slot {
    /// `GlobalId`.
    pub const GLOBAL_ID: usize = 0;
    /// `Name`.
    pub const NAME: usize = 2;
    /// `Description`.
    pub const DESCRIPTION: usize = 3;
    /// `ObjectPlacement`.
    pub const OBJECT_PLACEMENT: usize = 5;
    /// `Representation`.
    pub const REPRESENTATION: usize = 6;
    /// `LongName`, from `IfcSpatialElement`.
    pub const LONG_NAME: usize = 7;
    /// `PredefinedType`, declared by `IfcSpatialZone`.
    pub const PREDEFINED_TYPE: usize = 8;
    /// Total attribute count.
    pub const WIDTH: usize = 9;
}

/// A concrete distribution element class.
///
/// The variants are exactly the classes this crate's flow reader
/// traverses. Holding them in a closed enum means a caller cannot
/// stage a class the reader will later fail to recognise.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DistributionElementKind {
    /// `IfcDistributionElement`: the general form.
    DistributionElement,
    /// `IfcEnergyConversionDevice`: boilers, chillers, coils.
    EnergyConversionDevice,
    /// `IfcFlowController`: valves, dampers, switches.
    FlowController,
    /// `IfcFlowFitting`: junctions, bends, transitions.
    FlowFitting,
    /// `IfcFlowMovingDevice`: pumps, fans, compressors.
    FlowMovingDevice,
    /// `IfcFlowSegment`: a run of duct, pipe or cable.
    FlowSegment,
    /// `IfcFlowStorageDevice`: tanks and batteries.
    FlowStorageDevice,
    /// `IfcFlowTerminal`: the point where flow leaves the system.
    FlowTerminal,
    /// `IfcFlowTreatmentDevice`: filters, interceptors.
    FlowTreatmentDevice,
}

impl DistributionElementKind {
    /// The IFC entity name this kind stages.
    pub fn type_name(self) -> &'static str {
        match self {
            Self::DistributionElement => "IFCDISTRIBUTIONELEMENT",
            Self::EnergyConversionDevice => "IFCENERGYCONVERSIONDEVICE",
            Self::FlowController => "IFCFLOWCONTROLLER",
            Self::FlowFitting => "IFCFLOWFITTING",
            Self::FlowMovingDevice => "IFCFLOWMOVINGDEVICE",
            Self::FlowSegment => "IFCFLOWSEGMENT",
            Self::FlowStorageDevice => "IFCFLOWSTORAGEDEVICE",
            Self::FlowTerminal => "IFCFLOWTERMINAL",
            Self::FlowTreatmentDevice => "IFCFLOWTREATMENTDEVICE",
        }
    }
}

/// Optional descriptive attributes shared by every distribution element.
///
/// Grouped into a struct because all six are optional and positional
/// arguments at that count invite silent transposition -- `name` and
/// `description` are both `Option<&str>` and swapping them compiles.
#[derive(Debug, Clone, Copy, Default)]
pub struct ElementAttributes<'a> {
    /// `Name`.
    pub name: Option<&'a str>,
    /// `Description`.
    pub description: Option<&'a str>,
    /// `ObjectPlacement`, usually an `IfcLocalPlacement`.
    pub placement: Option<EntityId>,
    /// `Representation`, an `IfcProductDefinitionShape`.
    pub representation: Option<EntityId>,
    /// `Tag`, the authoring tool's own identifier.
    pub tag: Option<&'a str>,
}

fn text(value: Option<&str>) -> Value {
    value.map_or(Value::Null, |v| Value::Text(v.into()))
}

fn reference(value: Option<EntityId>) -> Value {
    value.map_or(Value::Null, Value::Ref)
}

/// Stage a distribution element occurrence.
///
/// Writes `OwnerHistory` `$`, so the record is IFC4/IFC4X3 only; use
/// [`create_distribution_element_with_owner_history`] for IFC2X3.
///
/// # Errors
///
/// Refuses a malformed GlobalId.
pub fn create_distribution_element(
    tx: &mut Transaction,
    kind: DistributionElementKind,
    global_id: &str,
    attributes: ElementAttributes<'_>,
) -> SystemAuthoringResult<EntityId> {
    let type_name = kind.type_name();
    super::guid(type_name, global_id)?;

    let mut attrs = vec![Value::Null; distribution_slot::WIDTH];
    attrs[distribution_slot::GLOBAL_ID] = Value::Text(global_id.into());
    attrs[distribution_slot::NAME] = text(attributes.name);
    attrs[distribution_slot::DESCRIPTION] = text(attributes.description);
    attrs[distribution_slot::OBJECT_PLACEMENT] = reference(attributes.placement);
    attrs[distribution_slot::REPRESENTATION] = reference(attributes.representation);
    attrs[distribution_slot::TAG] = text(attributes.tag);
    Ok(tx.create(Entity::new(type_name, attrs)))
}

/// Stage an `IfcZone`: spaces grouped for a shared purpose.
///
/// A zone holds no members directly. Membership is an
/// `IfcRelAssignsToGroup` pointing at this entity, which is why this
/// takes no member list -- staging one here would imply the zone
/// owns its contents, and it does not.
///
/// Writes the IFC4/IFC4X3 six-attribute layout with `OwnerHistory` `$`,
/// so the record is IFC4/IFC4X3 only; use
/// [`create_zone_with_owner_history`] for IFC2X3, which declares five
/// (no `LongName`).
///
/// # Errors
///
/// Refuses a malformed GlobalId.
pub fn create_zone(
    tx: &mut Transaction,
    global_id: &str,
    name: Option<&str>,
    description: Option<&str>,
    long_name: Option<&str>,
) -> SystemAuthoringResult<EntityId> {
    super::guid("IFCZONE", global_id)?;

    let mut attrs = vec![Value::Null; zone_slot::WIDTH];
    attrs[zone_slot::GLOBAL_ID] = Value::Text(global_id.into());
    attrs[zone_slot::NAME] = text(name);
    attrs[zone_slot::DESCRIPTION] = text(description);
    attrs[zone_slot::LONG_NAME] = text(long_name);
    Ok(tx.create(Entity::new("IFCZONE", attrs)))
}

/// Stage an `IfcSpatialZone`: a zone that occupies space.
///
/// # The USERDEFINED rule
///
/// `IfcSpatialZoneTypeEnum` includes `USERDEFINED`, which means
/// "the predefined list does not cover this". The schema pairs that
/// with `ObjectType` on `IfcObject`: choosing USERDEFINED without
/// naming the user-defined kind leaves the zone's purpose
/// unrecoverable, so it is refused here rather than written out as
/// a file nobody can interpret.
///
/// Writes `OwnerHistory` `$`, so the record is IFC4/IFC4X3 only; use
/// [`create_spatial_zone_with_owner_history`] for a release-bound record.
/// IFC2X3 declares no `IfcSpatialZone`.
///
/// # Errors
///
/// Refuses a malformed GlobalId, and USERDEFINED without an
/// `object_type` label.
pub fn create_spatial_zone(
    tx: &mut Transaction,
    global_id: &str,
    attributes: ElementAttributes<'_>,
    long_name: Option<&str>,
    predefined_type: Option<&str>,
    object_type: Option<&str>,
) -> SystemAuthoringResult<EntityId> {
    check_spatial_zone(global_id, predefined_type, object_type)?;

    let mut attrs = vec![Value::Null; spatial_zone_slot::WIDTH];
    attrs[spatial_zone_slot::GLOBAL_ID] = Value::Text(global_id.into());
    attrs[spatial_zone_slot::NAME] = text(attributes.name);
    attrs[spatial_zone_slot::DESCRIPTION] = text(attributes.description);
    attrs[4] = text(object_type);
    attrs[spatial_zone_slot::OBJECT_PLACEMENT] = reference(attributes.placement);
    attrs[spatial_zone_slot::REPRESENTATION] = reference(attributes.representation);
    attrs[spatial_zone_slot::LONG_NAME] = text(long_name);
    attrs[spatial_zone_slot::PREDEFINED_TYPE] =
        predefined_type.map_or(Value::Null, |t| Value::Enum(t.into()));
    Ok(tx.create(Entity::new("IFCSPATIALZONE", attrs)))
}

/// The plain spatial-zone checks, shared with the release-bound variant.
fn check_spatial_zone(
    global_id: &str,
    predefined_type: Option<&str>,
    object_type: Option<&str>,
) -> SystemAuthoringResult<()> {
    super::guid("IFCSPATIALZONE", global_id)?;
    if predefined_type == Some("USERDEFINED") && object_type.is_none() {
        return Err(invalid(
            "IFCSPATIALZONE",
            "PredefinedType",
            "USERDEFINED requires an ObjectType naming the kind",
        ));
    }
    Ok(())
}

/// [`create_distribution_element`] in `model`'s declared release, with a
/// caller-supplied `IfcOwnerHistory`, which IFC2X3 requires (#202).
///
/// Every [`DistributionElementKind`] declares the same eight attributes in
/// IFC2X3, IFC4 and IFC4X3; only `OwnerHistory` changes, from required to
/// optional. The owner history is never invented.
///
/// # Errors
///
/// Those of [`create_distribution_element`], and the release and
/// owner-history refusals of
/// [`create_system_with_owner_history`](super::create_system_with_owner_history).
/// Nothing is staged on an error.
pub fn create_distribution_element_with_owner_history(
    tx: &mut Transaction,
    model: &Model,
    kind: DistributionElementKind,
    global_id: &str,
    attributes: ElementAttributes<'_>,
    owner_history: EntityId,
) -> SystemAuthoringResult<EntityId> {
    let release = bind(model)?;
    let type_name = kind.type_name();
    super::guid(type_name, global_id)?;
    let values = vec![
        ("GlobalId", Value::Text(global_id.into())),
        ("Name", text(attributes.name)),
        ("Description", text(attributes.description)),
        ("ObjectPlacement", reference(attributes.placement)),
        ("Representation", reference(attributes.representation)),
        ("Tag", text(attributes.tag)),
    ];
    release.stage(tx, model, type_name, values, owner_history)
}

/// [`create_zone`] in `model`'s declared release, with a caller-supplied
/// `IfcOwnerHistory`, which IFC2X3 requires (#202).
///
/// IFC2X3 `IfcZone` declares no `LongName`, so a `long_name` there is
/// refused rather than dropped.
///
/// # Errors
///
/// Those of [`create_zone`], the release and owner-history refusals of
/// [`create_system_with_owner_history`](super::create_system_with_owner_history),
/// and [`AuthoringNotInSchema`](super::SystemAuthoringError::AuthoringNotInSchema)
/// for an IFC2X3 `long_name`. Nothing is staged on an error.
pub fn create_zone_with_owner_history(
    tx: &mut Transaction,
    model: &Model,
    global_id: &str,
    name: Option<&str>,
    description: Option<&str>,
    long_name: Option<&str>,
    owner_history: EntityId,
) -> SystemAuthoringResult<EntityId> {
    let release = bind(model)?;
    super::guid("IFCZONE", global_id)?;
    let values = vec![
        ("GlobalId", Value::Text(global_id.into())),
        ("Name", text(name)),
        ("Description", text(description)),
        ("LongName", text(long_name)),
    ];
    release.stage(tx, model, "IFCZONE", values, owner_history)
}

/// [`create_spatial_zone`] in `model`'s declared release, with a
/// caller-supplied `IfcOwnerHistory` (#202).
///
/// `PredefinedType` is checked against the release's own
/// `IfcSpatialZoneTypeEnum`: IFC4X3 adds `INTERFERENCE` and `RESERVATION`,
/// which IFC4 refuses.
///
/// # Errors
///
/// Those of [`create_spatial_zone`], the release and owner-history
/// refusals of
/// [`create_system_with_owner_history`](super::create_system_with_owner_history),
/// [`EntityNotInSchema`](super::SystemAuthoringError::EntityNotInSchema) in
/// IFC2X3, and
/// [`AuthoringValueType`](super::SystemAuthoringError::AuthoringValueType)
/// for a token the release does not declare. Nothing is staged on an error.
#[allow(clippy::too_many_arguments)]
pub fn create_spatial_zone_with_owner_history(
    tx: &mut Transaction,
    model: &Model,
    global_id: &str,
    attributes: ElementAttributes<'_>,
    long_name: Option<&str>,
    predefined_type: Option<&str>,
    object_type: Option<&str>,
    owner_history: EntityId,
) -> SystemAuthoringResult<EntityId> {
    const ENTITY: &str = "IFCSPATIALZONE";
    let release = bind(model)?;
    release.require_entity(ENTITY)?;
    check_spatial_zone(global_id, predefined_type, object_type)?;
    let values = vec![
        ("GlobalId", Value::Text(global_id.into())),
        ("Name", text(attributes.name)),
        ("Description", text(attributes.description)),
        ("ObjectType", text(object_type)),
        ("ObjectPlacement", reference(attributes.placement)),
        ("Representation", reference(attributes.representation)),
        ("LongName", text(long_name)),
        (
            "PredefinedType",
            predefined_type.map_or(Value::Null, |t| Value::Enum(t.into())),
        ),
    ];
    release.stage(tx, model, ENTITY, values, owner_history)
}