ifc_spatial/authoring/connections.rs
1//! The pair-valued relationships: two elements, never a set.
2//!
3//! A few relationships connect exactly two elements --
4//! `IfcRelConnectsElements` and its subtypes, `IfcRelInterferesElements`.
5//! A set-shaped writer would accept a one-element or three-element list
6//! for those and produce a record no reader can interpret, so they get
7//! their own constructors taking two `EntityId`s. The refusal that matters
8//! there is self-connection: an element connected to itself is a cycle the
9//! connectivity reader in this crate will follow forever.
10
11use ifc_model::guid::Guid;
12use ifc_model::{Entity, EntityId, Transaction, Value};
13
14use crate::authoring::{invalid, SpatialAuthoringResult};
15use crate::relation::slots::{
16 RelSlots, CONNECTS_ELEMENTS, CONNECTS_WITH_REALIZING, INTERFERES_ELEMENTS,
17};
18
19/// Refuse an empty `RealizingElements`: the subtype exists to name them.
20pub(super) fn check_realizing(realizing: &[EntityId]) -> SpatialAuthoringResult<()> {
21 if realizing.is_empty() {
22 return Err(invalid(
23 CONNECTS_WITH_REALIZING.type_name,
24 "RealizingElements",
25 "empty",
26 ));
27 }
28 Ok(())
29}
30
31/// Refuse a relationship that connects an element to itself.
32///
33/// The connectivity reader walks these as a graph. A self-edge is
34/// not a harmless oddity there: it is a cycle of length one.
35pub(super) fn distinct(
36 rel: RelSlots,
37 global_id: &str,
38 relating: EntityId,
39 related: EntityId,
40) -> SpatialAuthoringResult<()> {
41 if Guid::parse(global_id).is_none() {
42 return Err(invalid(rel.type_name, "GlobalId", global_id));
43 }
44 if relating == related {
45 return Err(invalid(
46 rel.type_name,
47 "RelatedElement",
48 "an element cannot connect to itself",
49 ));
50 }
51 Ok(())
52}
53
54fn pair(
55 tx: &mut Transaction,
56 rel: RelSlots,
57 global_id: &str,
58 relating: EntityId,
59 related: EntityId,
60 width: usize,
61) -> SpatialAuthoringResult<EntityId> {
62 distinct(rel, global_id, relating, related)?;
63
64 let mut attributes = vec![Value::Null; width];
65 attributes[0] = Value::Text(global_id.into());
66 attributes[rel.relating] = Value::Ref(relating);
67 attributes[rel.related] = Value::Ref(related);
68 Ok(tx.create(Entity::new(rel.type_name, attributes)))
69}
70
71/// Stage an `IfcRelConnectsElements`: two elements physically joined.
72///
73/// IFC4 and IFC4X3 only: it writes their layout and leaves
74/// `OwnerHistory` `$`, which IFC2X3 requires. In IFC2X3 use
75/// [`connect_elements_with_owner_history`](super::connect_elements_with_owner_history), which binds the model's declared
76/// release.
77///
78/// # Errors
79///
80/// Refuses a malformed GlobalId and an element connected to itself.
81pub fn connect_elements(
82 tx: &mut Transaction,
83 global_id: &str,
84 relating: EntityId,
85 related: EntityId,
86) -> SpatialAuthoringResult<EntityId> {
87 pair(tx, CONNECTS_ELEMENTS, global_id, relating, related, 7)
88}
89
90/// Stage an `IfcRelConnectsWithRealizingElements`.
91///
92/// The realizing elements are what physically make the connection
93/// -- a weld, a bolt, a bracket.
94///
95/// IFC4 and IFC4X3 only: it writes their layout and leaves
96/// `OwnerHistory` `$`, which IFC2X3 requires. In IFC2X3 use
97/// [`connect_with_realizing_elements_with_owner_history`](super::connect_with_realizing_elements_with_owner_history), which binds the model's declared
98/// release.
99///
100/// # Errors
101///
102/// Refuses a malformed GlobalId, an element connected to itself,
103/// and an empty realizing set: the subtype exists precisely to name
104/// those elements, so omitting them makes it an
105/// `IfcRelConnectsElements` wearing the wrong type name.
106pub fn connect_with_realizing_elements(
107 tx: &mut Transaction,
108 global_id: &str,
109 relating: EntityId,
110 related: EntityId,
111 realizing: &[EntityId],
112) -> SpatialAuthoringResult<EntityId> {
113 check_realizing(realizing)?;
114 let id = pair(tx, CONNECTS_WITH_REALIZING, global_id, relating, related, 8)?;
115 tx.set_attribute(
116 id,
117 REALIZING_SLOT,
118 Value::List(realizing.iter().copied().map(Value::Ref).collect()),
119 );
120 Ok(id)
121}
122
123/// `RealizingElements` on `IfcRelConnectsWithRealizingElements`.
124const REALIZING_SLOT: usize = 7;
125
126/// Stage an `IfcRelInterferesElements`: a detected clash.
127///
128/// `implied_order` is `ImpliedOrder`, an `IfcLogical` at slot 8. It
129/// says whether the relating/related order carries meaning (which
130/// element gives way). `None` writes UNKNOWN, which is the honest
131/// value when a clash detector reports an overlap without deciding
132/// precedence.
133///
134/// IFC4 and IFC4X3 only: it writes their layout and leaves
135/// `OwnerHistory` `$`, which IFC2X3 requires. In IFC2X3 use
136/// [`interfere_elements_with_owner_history`](super::interfere_elements_with_owner_history), which binds the model's declared
137/// release.
138///
139/// # Errors
140///
141/// Refuses a malformed GlobalId and an element interfering with
142/// itself.
143pub fn interfere_elements(
144 tx: &mut Transaction,
145 global_id: &str,
146 relating: EntityId,
147 related: EntityId,
148 implied_order: Option<bool>,
149) -> SpatialAuthoringResult<EntityId> {
150 let id = pair(tx, INTERFERES_ELEMENTS, global_id, relating, related, 10)?;
151 tx.set_attribute(
152 id,
153 IMPLIED_ORDER_SLOT,
154 implied_order.map_or(Value::LogicalUnknown, Value::Bool),
155 );
156 Ok(id)
157}
158
159/// `ImpliedOrder` on `IfcRelInterferesElements`.
160const IMPLIED_ORDER_SLOT: usize = 8;