ifc_structural/authoring/
relation.rs1use 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#[derive(Debug, Clone)]
14pub struct RelationshipRootDraft {
15 pub global_id: String,
17 pub owner_history: Option<EntityId>,
19 pub name: Option<String>,
21 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#[derive(Debug, Clone)]
38pub struct MemberConnectionDraft {
39 pub root: RelationshipRootDraft,
41 pub member: EntityId,
43 pub connection: EntityId,
45 pub applied_condition: Option<EntityId>,
47 pub additional_conditions: Option<EntityId>,
49 pub supported_length: Option<f64>,
51 pub condition_coordinate_system: Option<EntityId>,
53 pub eccentricity: Option<EntityId>,
59}
60
61#[derive(Debug, Clone)]
63pub struct ActivityAssignmentDraft {
64 pub root: RelationshipRootDraft,
66 pub relating_element: EntityId,
68 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
93pub fn stage_member_connection(
98 tx: &mut Transaction,
99 model: &Model,
100 schema: &Schema,
101 draft: MemberConnectionDraft,
102) -> StructuralResult<EntityId> {
103 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
182pub 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}