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#[derive(Debug, Clone)]
14#[non_exhaustive]
15pub struct RelationshipRootDraft {
16 pub global_id: String,
18 pub owner_history: Option<EntityId>,
20 pub name: Option<String>,
22 pub description: Option<String>,
24}
25
26impl RelationshipRootDraft {
27 #[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 #[must_use]
41 pub fn owner_history(mut self, value: EntityId) -> Self {
42 self.owner_history = Some(value);
43 self
44 }
45
46 #[must_use]
48 pub fn name(mut self, value: impl Into<String>) -> Self {
49 self.name = Some(value.into());
50 self
51 }
52
53 #[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#[derive(Debug, Clone)]
74#[non_exhaustive]
75pub struct MemberConnectionDraft {
76 pub root: RelationshipRootDraft,
78 pub member: EntityId,
80 pub connection: EntityId,
82 pub applied_condition: Option<EntityId>,
84 pub additional_conditions: Option<EntityId>,
86 pub supported_length: Option<f64>,
88 pub condition_coordinate_system: Option<EntityId>,
90 pub eccentricity: Option<EntityId>,
96}
97
98impl MemberConnectionDraft {
99 #[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 #[must_use]
117 pub fn applied_condition(mut self, value: EntityId) -> Self {
118 self.applied_condition = Some(value);
119 self
120 }
121
122 #[must_use]
125 pub fn additional_conditions(mut self, value: EntityId) -> Self {
126 self.additional_conditions = Some(value);
127 self
128 }
129
130 #[must_use]
133 pub fn supported_length(mut self, value: f64) -> Self {
134 self.supported_length = Some(value);
135 self
136 }
137
138 #[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 #[must_use]
151 pub fn eccentricity(mut self, value: EntityId) -> Self {
152 self.eccentricity = Some(value);
153 self
154 }
155}
156
157#[derive(Debug, Clone)]
159#[non_exhaustive]
160pub struct ActivityAssignmentDraft {
161 pub root: RelationshipRootDraft,
163 pub relating_element: EntityId,
165 pub activity: EntityId,
167}
168
169impl ActivityAssignmentDraft {
170 #[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
206pub fn stage_member_connection(
211 tx: &mut Transaction,
212 model: &Model,
213 schema: &Schema,
214 draft: MemberConnectionDraft,
215) -> StructuralResult<EntityId> {
216 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
295pub 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}