Skip to main content

ifc_structural/authoring/
relation.rs

1use std::collections::HashSet;
2
3use ifc_model::guid::Guid;
4use ifc_model::{Edit, EntityId, Model, Transaction, Value};
5use ifc_schema::Schema;
6
7use super::{
8    build_named, optional_ref, optional_text, projected_entity, validate_optional_ref, validate_ref,
9};
10use crate::error::{StructuralError, StructuralResult};
11
12/// Staged `IfcRoot`-level attributes shared by every staged structural relationship.
13#[derive(Debug, Clone)]
14pub struct RelationshipRootDraft {
15    /// `GlobalId`; must parse as a 22-character IFC GUID.
16    pub global_id: String,
17    /// `OwnerHistory`, validated against the model/transaction if present.
18    pub owner_history: Option<EntityId>,
19    /// `Name`.
20    pub name: Option<String>,
21    /// `Description`.
22    pub description: Option<String>,
23}
24
25impl Default for RelationshipRootDraft {
26    fn default() -> Self {
27        Self {
28            global_id: "0000000000000000000000".into(),
29            owner_history: None,
30            name: None,
31            description: None,
32        }
33    }
34}
35
36/// Staged fields for creating an `IfcRelConnectsStructuralMember` via [`stage_member_connection`].
37#[derive(Debug, Clone)]
38pub struct MemberConnectionDraft {
39    /// `IfcRoot` attributes shared with other staged structural relationships.
40    pub root: RelationshipRootDraft,
41    /// `RelatingStructuralMember`.
42    pub member: EntityId,
43    /// `RelatedStructuralConnection`.
44    pub connection: EntityId,
45    /// `AppliedCondition`, an `IfcBoundaryCondition` reference.
46    pub applied_condition: Option<EntityId>,
47    /// `AdditionalConditions`, an `IfcStructuralConnectionCondition` reference.
48    pub additional_conditions: Option<EntityId>,
49    /// `SupportedLength`; must be a positive finite value when set.
50    pub supported_length: Option<f64>,
51    /// `ConditionCoordinateSystem`, an `IfcAxis2Placement3D` reference.
52    pub condition_coordinate_system: Option<EntityId>,
53    /// `ConnectionConstraint`, an `IfcConnectionGeometry` reference.
54    ///
55    /// Selects `IfcRelConnectsWithEccentricity`, whose only added slot
56    /// this is. The slot is required there, so a subtype cannot be
57    /// staged without it and the base cannot carry it.
58    pub eccentricity: Option<EntityId>,
59}
60
61/// Staged fields for creating an `IfcRelConnectsStructuralActivity` via [`stage_activity_assignment`].
62#[derive(Debug, Clone)]
63pub struct ActivityAssignmentDraft {
64    /// `IfcRoot` attributes shared with other staged structural relationships.
65    pub root: RelationshipRootDraft,
66    /// `RelatingElement`, an `IfcStructuralActivityAssignmentSelect` (`IfcElement` or `IfcStructuralItem`).
67    pub relating_element: EntityId,
68    /// `RelatedStructuralActivity`, the `IfcStructuralActivity` being attached.
69    pub activity: EntityId,
70}
71
72fn validate_root(
73    tx: &Transaction,
74    model: &Model,
75    schema: &Schema,
76    root: &RelationshipRootDraft,
77) -> StructuralResult<()> {
78    if Guid::parse(&root.global_id).is_none() {
79        return Err(StructuralError::InvalidGlobalId);
80    }
81    validate_optional_ref(tx, model, schema, root.owner_history, "IfcOwnerHistory")
82}
83
84fn root_fields(root: RelationshipRootDraft) -> Vec<(&'static str, Value)> {
85    vec![
86        ("GlobalId", Value::Text(root.global_id.into())),
87        ("OwnerHistory", optional_ref(root.owner_history)),
88        ("Name", optional_text(root.name)),
89        ("Description", optional_text(root.description)),
90    ]
91}
92
93/// Stage an `IfcRelConnectsStructuralMember` create edit on `tx`.
94///
95/// Fails with [`StructuralError::InvalidDraftValue`] if `supported_length` is
96/// set but not positive and finite. Returns the id staged for the new entity.
97pub fn stage_member_connection(
98    tx: &mut Transaction,
99    model: &Model,
100    schema: &Schema,
101    draft: MemberConnectionDraft,
102) -> StructuralResult<EntityId> {
103    // The eccentric form differs only by a required ConnectionConstraint,
104    // so the presence of one selects the subtype.
105    let entity = if draft.eccentricity.is_some() {
106        "IfcRelConnectsWithEccentricity"
107    } else {
108        "IfcRelConnectsStructuralMember"
109    };
110    const ENTITY: &str = "IfcRelConnectsStructuralMember";
111    validate_root(tx, model, schema, &draft.root)?;
112    validate_ref(tx, model, schema, draft.member, "IfcStructuralMember")?;
113    validate_ref(
114        tx,
115        model,
116        schema,
117        draft.connection,
118        "IfcStructuralConnection",
119    )?;
120    validate_optional_ref(
121        tx,
122        model,
123        schema,
124        draft.applied_condition,
125        "IfcBoundaryCondition",
126    )?;
127    validate_optional_ref(
128        tx,
129        model,
130        schema,
131        draft.additional_conditions,
132        "IfcStructuralConnectionCondition",
133    )?;
134    validate_optional_ref(
135        tx,
136        model,
137        schema,
138        draft.condition_coordinate_system,
139        "IfcAxis2Placement3D",
140    )?;
141    validate_optional_ref(
142        tx,
143        model,
144        schema,
145        draft.eccentricity,
146        "IfcConnectionGeometry",
147    )?;
148    if draft
149        .supported_length
150        .is_some_and(|value| !value.is_finite() || value <= 0.0)
151    {
152        return Err(StructuralError::InvalidDraftValue {
153            entity_type: ENTITY,
154            attribute: "SupportedLength",
155            expected: "positive finite length or null",
156        });
157    }
158    let mut fields = root_fields(draft.root);
159    fields.extend([
160        ("RelatingStructuralMember", Value::Ref(draft.member)),
161        ("RelatedStructuralConnection", Value::Ref(draft.connection)),
162        ("AppliedCondition", optional_ref(draft.applied_condition)),
163        (
164            "AdditionalConditions",
165            optional_ref(draft.additional_conditions),
166        ),
167        (
168            "SupportedLength",
169            draft.supported_length.map_or(Value::Null, Value::Real),
170        ),
171        (
172            "ConditionCoordinateSystem",
173            optional_ref(draft.condition_coordinate_system),
174        ),
175    ]);
176    if let Some(constraint) = draft.eccentricity {
177        fields.push(("ConnectionConstraint", Value::Ref(constraint)));
178    }
179    Ok(tx.create(build_named(schema, entity, fields)?))
180}
181
182/// Stage an `IfcRelConnectsStructuralActivity` create edit on `tx`.
183///
184/// Fails with [`StructuralError::DanglingReference`] if `relating_element`
185/// resolves to nothing, [`StructuralError::WrongReferenceType`] if it is not
186/// an `IfcStructuralActivityAssignmentSelect` member, and with
187/// [`StructuralError::SemanticViolation`] if `activity` already has an
188/// attaching `IfcRelConnectsStructuralActivity` relation (staged or existing).
189/// Returns the id staged for the new entity.
190pub fn stage_activity_assignment(
191    tx: &mut Transaction,
192    model: &Model,
193    schema: &Schema,
194    draft: ActivityAssignmentDraft,
195) -> StructuralResult<EntityId> {
196    const ENTITY: &str = "IfcRelConnectsStructuralActivity";
197    validate_root(tx, model, schema, &draft.root)?;
198    let relating = projected_entity(tx, model, draft.relating_element).ok_or(
199        StructuralError::DanglingReference {
200            entity: EntityId(0),
201            attribute: "draft reference",
202            target: draft.relating_element,
203        },
204    )?;
205    if !schema.accepts_type("IfcStructuralActivityAssignmentSelect", &relating.type_name) {
206        return Err(StructuralError::WrongReferenceType {
207            entity: EntityId(0),
208            attribute: "draft reference",
209            target: draft.relating_element,
210            expected: "IfcStructuralActivityAssignmentSelect",
211            actual: relating.type_name.to_string(),
212        });
213    }
214    validate_ref(tx, model, schema, draft.activity, "IfcStructuralActivity")?;
215    if has_activity_attachment(tx, model, schema, draft.activity) {
216        return Err(StructuralError::SemanticViolation {
217            entity: Some(draft.activity),
218            rule: "IfcStructuralActivity.AssignedToStructuralItem SET [0:1]",
219        });
220    }
221    let mut fields = root_fields(draft.root);
222    fields.extend([
223        ("RelatingElement", Value::Ref(draft.relating_element)),
224        ("RelatedStructuralActivity", Value::Ref(draft.activity)),
225    ]);
226    Ok(tx.create(build_named(schema, ENTITY, fields)?))
227}
228
229fn has_activity_attachment(
230    tx: &Transaction,
231    model: &Model,
232    schema: &Schema,
233    activity: EntityId,
234) -> bool {
235    let mut ids: HashSet<EntityId> = model.iter().map(|(id, _)| id).collect();
236    for edit in tx.edits() {
237        if let Edit::Create { id, .. } = edit {
238            ids.insert(*id);
239        }
240    }
241    ids.into_iter().any(|id| {
242        let Some(entity) = projected_entity(tx, model, id) else { return false; };
243        if !schema.is_a(&entity.type_name, "IfcRelConnectsStructuralActivity") { return false; }
244        let Some(slot) = schema.attributes(&entity.type_name).iter()
245            .position(|attribute| attribute.name.eq_ignore_ascii_case("RelatedStructuralActivity"))
246        else { return false; };
247        matches!(entity.attribute(slot).map(Value::unwrap_typed), Some(Value::Ref(target)) if *target == activity)
248    })
249}