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)]
14#[non_exhaustive]
15pub struct RelationshipRootDraft {
16    /// `GlobalId`; must parse as a 22-character IFC GUID.
17    pub global_id: String,
18    /// `OwnerHistory`, validated against the model/transaction if present.
19    pub owner_history: Option<EntityId>,
20    /// `Name`.
21    pub name: Option<String>,
22    /// `Description`.
23    pub description: Option<String>,
24}
25
26impl RelationshipRootDraft {
27    /// Starts a draft from its required fields; every other field is unset.
28    #[must_use]
29    pub fn new(global_id: impl Into<String>) -> Self {
30        Self {
31            global_id: global_id.into(),
32            owner_history: None,
33            name: None,
34            description: None,
35        }
36    }
37
38    /// Sets `owner_history`: `OwnerHistory`, validated against the
39    /// model/transaction if present.
40    #[must_use]
41    pub fn owner_history(mut self, value: EntityId) -> Self {
42        self.owner_history = Some(value);
43        self
44    }
45
46    /// Sets `name`: `Name`.
47    #[must_use]
48    pub fn name(mut self, value: impl Into<String>) -> Self {
49        self.name = Some(value.into());
50        self
51    }
52
53    /// Sets `description`: `Description`.
54    #[must_use]
55    pub fn description(mut self, value: impl Into<String>) -> Self {
56        self.description = Some(value.into());
57        self
58    }
59}
60
61impl Default for RelationshipRootDraft {
62    fn default() -> Self {
63        Self {
64            global_id: "0000000000000000000000".into(),
65            owner_history: None,
66            name: None,
67            description: None,
68        }
69    }
70}
71
72/// Staged fields for creating an `IfcRelConnectsStructuralMember` via [`stage_member_connection`].
73#[derive(Debug, Clone)]
74#[non_exhaustive]
75pub struct MemberConnectionDraft {
76    /// `IfcRoot` attributes shared with other staged structural relationships.
77    pub root: RelationshipRootDraft,
78    /// `RelatingStructuralMember`.
79    pub member: EntityId,
80    /// `RelatedStructuralConnection`.
81    pub connection: EntityId,
82    /// `AppliedCondition`, an `IfcBoundaryCondition` reference.
83    pub applied_condition: Option<EntityId>,
84    /// `AdditionalConditions`, an `IfcStructuralConnectionCondition` reference.
85    pub additional_conditions: Option<EntityId>,
86    /// `SupportedLength`; must be a positive finite value when set.
87    pub supported_length: Option<f64>,
88    /// `ConditionCoordinateSystem`, an `IfcAxis2Placement3D` reference.
89    pub condition_coordinate_system: Option<EntityId>,
90    /// `ConnectionConstraint`, an `IfcConnectionGeometry` reference.
91    ///
92    /// Selects `IfcRelConnectsWithEccentricity`, whose only added slot
93    /// this is. The slot is required there, so a subtype cannot be
94    /// staged without it and the base cannot carry it.
95    pub eccentricity: Option<EntityId>,
96}
97
98impl MemberConnectionDraft {
99    /// Starts a draft from its required fields; every other field is unset.
100    #[must_use]
101    pub fn new(root: RelationshipRootDraft, member: EntityId, connection: EntityId) -> Self {
102        Self {
103            root,
104            member,
105            connection,
106            applied_condition: None,
107            additional_conditions: None,
108            supported_length: None,
109            condition_coordinate_system: None,
110            eccentricity: None,
111        }
112    }
113
114    /// Sets `applied_condition`: `AppliedCondition`, an `IfcBoundaryCondition`
115    /// reference.
116    #[must_use]
117    pub fn applied_condition(mut self, value: EntityId) -> Self {
118        self.applied_condition = Some(value);
119        self
120    }
121
122    /// Sets `additional_conditions`: `AdditionalConditions`, an
123    /// `IfcStructuralConnectionCondition` reference.
124    #[must_use]
125    pub fn additional_conditions(mut self, value: EntityId) -> Self {
126        self.additional_conditions = Some(value);
127        self
128    }
129
130    /// Sets `supported_length`: `SupportedLength`; must be a positive finite
131    /// value when set.
132    #[must_use]
133    pub fn supported_length(mut self, value: f64) -> Self {
134        self.supported_length = Some(value);
135        self
136    }
137
138    /// Sets `condition_coordinate_system`: `ConditionCoordinateSystem`, an
139    /// `IfcAxis2Placement3D` reference.
140    #[must_use]
141    pub fn condition_coordinate_system(mut self, value: EntityId) -> Self {
142        self.condition_coordinate_system = Some(value);
143        self
144    }
145
146    /// Sets `eccentricity`: `ConnectionConstraint`, an `IfcConnectionGeometry`
147    /// reference.  Selects `IfcRelConnectsWithEccentricity`, whose only added
148    /// slot this is. The slot is required there, so a subtype cannot be staged
149    /// without it and the base cannot carry it.
150    #[must_use]
151    pub fn eccentricity(mut self, value: EntityId) -> Self {
152        self.eccentricity = Some(value);
153        self
154    }
155}
156
157/// Staged fields for creating an `IfcRelConnectsStructuralActivity` via [`stage_activity_assignment`].
158#[derive(Debug, Clone)]
159#[non_exhaustive]
160pub struct ActivityAssignmentDraft {
161    /// `IfcRoot` attributes shared with other staged structural relationships.
162    pub root: RelationshipRootDraft,
163    /// `RelatingElement`, an `IfcStructuralActivityAssignmentSelect` (`IfcElement` or `IfcStructuralItem`).
164    pub relating_element: EntityId,
165    /// `RelatedStructuralActivity`, the `IfcStructuralActivity` being attached.
166    pub activity: EntityId,
167}
168
169impl ActivityAssignmentDraft {
170    /// Starts a draft from its required fields; every other field is unset.
171    #[must_use]
172    pub fn new(
173        root: RelationshipRootDraft,
174        relating_element: EntityId,
175        activity: EntityId,
176    ) -> Self {
177        Self {
178            root,
179            relating_element,
180            activity,
181        }
182    }
183}
184
185fn validate_root(
186    tx: &Transaction,
187    model: &Model,
188    schema: &Schema,
189    root: &RelationshipRootDraft,
190) -> StructuralResult<()> {
191    if Guid::parse(&root.global_id).is_none() {
192        return Err(StructuralError::InvalidGlobalId);
193    }
194    validate_optional_ref(tx, model, schema, root.owner_history, "IfcOwnerHistory")
195}
196
197fn root_fields(root: RelationshipRootDraft) -> Vec<(&'static str, Value)> {
198    vec![
199        ("GlobalId", Value::Text(root.global_id.into())),
200        ("OwnerHistory", optional_ref(root.owner_history)),
201        ("Name", optional_text(root.name)),
202        ("Description", optional_text(root.description)),
203    ]
204}
205
206/// Stage an `IfcRelConnectsStructuralMember` create edit on `tx`.
207///
208/// Fails with [`StructuralError::InvalidDraftValue`] if `supported_length` is
209/// set but not positive and finite. Returns the id staged for the new entity.
210pub fn stage_member_connection(
211    tx: &mut Transaction,
212    model: &Model,
213    schema: &Schema,
214    draft: MemberConnectionDraft,
215) -> StructuralResult<EntityId> {
216    // The eccentric form differs only by a required ConnectionConstraint,
217    // so the presence of one selects the subtype.
218    let entity = if draft.eccentricity.is_some() {
219        "IfcRelConnectsWithEccentricity"
220    } else {
221        "IfcRelConnectsStructuralMember"
222    };
223    const ENTITY: &str = "IfcRelConnectsStructuralMember";
224    validate_root(tx, model, schema, &draft.root)?;
225    validate_ref(tx, model, schema, draft.member, "IfcStructuralMember")?;
226    validate_ref(
227        tx,
228        model,
229        schema,
230        draft.connection,
231        "IfcStructuralConnection",
232    )?;
233    validate_optional_ref(
234        tx,
235        model,
236        schema,
237        draft.applied_condition,
238        "IfcBoundaryCondition",
239    )?;
240    validate_optional_ref(
241        tx,
242        model,
243        schema,
244        draft.additional_conditions,
245        "IfcStructuralConnectionCondition",
246    )?;
247    validate_optional_ref(
248        tx,
249        model,
250        schema,
251        draft.condition_coordinate_system,
252        "IfcAxis2Placement3D",
253    )?;
254    validate_optional_ref(
255        tx,
256        model,
257        schema,
258        draft.eccentricity,
259        "IfcConnectionGeometry",
260    )?;
261    if draft
262        .supported_length
263        .is_some_and(|value| !value.is_finite() || value <= 0.0)
264    {
265        return Err(StructuralError::InvalidDraftValue {
266            entity_type: ENTITY,
267            attribute: "SupportedLength",
268            expected: "positive finite length or null",
269        });
270    }
271    let mut fields = root_fields(draft.root);
272    fields.extend([
273        ("RelatingStructuralMember", Value::Ref(draft.member)),
274        ("RelatedStructuralConnection", Value::Ref(draft.connection)),
275        ("AppliedCondition", optional_ref(draft.applied_condition)),
276        (
277            "AdditionalConditions",
278            optional_ref(draft.additional_conditions),
279        ),
280        (
281            "SupportedLength",
282            draft.supported_length.map_or(Value::Null, Value::Real),
283        ),
284        (
285            "ConditionCoordinateSystem",
286            optional_ref(draft.condition_coordinate_system),
287        ),
288    ]);
289    if let Some(constraint) = draft.eccentricity {
290        fields.push(("ConnectionConstraint", Value::Ref(constraint)));
291    }
292    Ok(tx.create(build_named(schema, entity, fields)?))
293}
294
295/// Stage an `IfcRelConnectsStructuralActivity` create edit on `tx`.
296///
297/// Fails with [`StructuralError::DanglingReference`] if `relating_element`
298/// resolves to nothing, [`StructuralError::WrongReferenceType`] if it is not
299/// an `IfcStructuralActivityAssignmentSelect` member, and with
300/// [`StructuralError::SemanticViolation`] if `activity` already has an
301/// attaching `IfcRelConnectsStructuralActivity` relation (staged or existing).
302/// Returns the id staged for the new entity.
303pub fn stage_activity_assignment(
304    tx: &mut Transaction,
305    model: &Model,
306    schema: &Schema,
307    draft: ActivityAssignmentDraft,
308) -> StructuralResult<EntityId> {
309    const ENTITY: &str = "IfcRelConnectsStructuralActivity";
310    validate_root(tx, model, schema, &draft.root)?;
311    let relating = projected_entity(tx, model, draft.relating_element).ok_or(
312        StructuralError::DanglingReference {
313            entity: EntityId(0),
314            attribute: "draft reference",
315            target: draft.relating_element,
316        },
317    )?;
318    if !schema.accepts_type("IfcStructuralActivityAssignmentSelect", &relating.type_name) {
319        return Err(StructuralError::WrongReferenceType {
320            entity: EntityId(0),
321            attribute: "draft reference",
322            target: draft.relating_element,
323            expected: "IfcStructuralActivityAssignmentSelect",
324            actual: relating.type_name.to_string(),
325        });
326    }
327    validate_ref(tx, model, schema, draft.activity, "IfcStructuralActivity")?;
328    if has_activity_attachment(tx, model, schema, draft.activity) {
329        return Err(StructuralError::SemanticViolation {
330            entity: Some(draft.activity),
331            rule: "IfcStructuralActivity.AssignedToStructuralItem SET [0:1]",
332        });
333    }
334    let mut fields = root_fields(draft.root);
335    fields.extend([
336        ("RelatingElement", Value::Ref(draft.relating_element)),
337        ("RelatedStructuralActivity", Value::Ref(draft.activity)),
338    ]);
339    Ok(tx.create(build_named(schema, ENTITY, fields)?))
340}
341
342fn has_activity_attachment(
343    tx: &Transaction,
344    model: &Model,
345    schema: &Schema,
346    activity: EntityId,
347) -> bool {
348    let mut ids: HashSet<EntityId> = model.iter().map(|(id, _)| id).collect();
349    for edit in tx.edits() {
350        if let Edit::Create { id, .. } = edit {
351            ids.insert(*id);
352        }
353    }
354    ids.into_iter().any(|id| {
355        let Some(entity) = projected_entity(tx, model, id) else { return false; };
356        if !schema.is_a(&entity.type_name, "IfcRelConnectsStructuralActivity") { return false; }
357        let Some(slot) = schema.attributes(&entity.type_name).iter()
358            .position(|attribute| attribute.name.eq_ignore_ascii_case("RelatedStructuralActivity"))
359        else { return false; };
360        matches!(entity.attribute(slot).map(Value::unwrap_typed), Some(Value::Ref(target)) if *target == activity)
361    })
362}