ifc_structural/authoring/
load_group.rs1use ifc_model::guid::Guid;
26use ifc_model::{EntityId, Model, Transaction, Value};
27use ifc_schema::{Schema, TypeKind};
28
29use super::{build_named, optional_text, validate_optional_ref};
30use crate::error::{StructuralError, StructuralResult};
31
32#[derive(Debug, Clone, Copy, PartialEq)]
34pub enum LoadGroupKind {
35 Group {
37 predefined_type: &'static str,
39 },
40 Case {
43 self_weight_coefficients: Option<[f64; 3]>,
49 },
50}
51
52#[derive(Debug, Clone)]
58#[non_exhaustive]
59pub struct LoadGroupDraft {
60 pub global_id: String,
62 pub owner_history: Option<EntityId>,
64 pub name: Option<String>,
66 pub description: Option<String>,
68 pub object_type: Option<String>,
71 pub action_type: &'static str,
73 pub action_source: &'static str,
75 pub coefficient: Option<f64>,
77 pub purpose: Option<String>,
79 pub kind: LoadGroupKind,
81}
82
83impl LoadGroupDraft {
84 #[must_use]
86 pub fn new(
87 global_id: impl Into<String>,
88 action_type: &'static str,
89 action_source: &'static str,
90 kind: LoadGroupKind,
91 ) -> Self {
92 Self {
93 global_id: global_id.into(),
94 owner_history: None,
95 name: None,
96 description: None,
97 object_type: None,
98 action_type,
99 action_source,
100 coefficient: None,
101 purpose: None,
102 kind,
103 }
104 }
105
106 #[must_use]
109 pub fn owner_history(mut self, value: EntityId) -> Self {
110 self.owner_history = Some(value);
111 self
112 }
113
114 #[must_use]
116 pub fn name(mut self, value: impl Into<String>) -> Self {
117 self.name = Some(value.into());
118 self
119 }
120
121 #[must_use]
123 pub fn description(mut self, value: impl Into<String>) -> Self {
124 self.description = Some(value.into());
125 self
126 }
127
128 #[must_use]
131 pub fn object_type(mut self, value: impl Into<String>) -> Self {
132 self.object_type = Some(value.into());
133 self
134 }
135
136 #[must_use]
139 pub fn coefficient(mut self, value: f64) -> Self {
140 self.coefficient = Some(value);
141 self
142 }
143
144 #[must_use]
146 pub fn purpose(mut self, value: impl Into<String>) -> Self {
147 self.purpose = Some(value.into());
148 self
149 }
150}
151
152pub fn stage_load_group(
163 tx: &mut Transaction,
164 model: &Model,
165 schema: &Schema,
166 draft: LoadGroupDraft,
167) -> StructuralResult<EntityId> {
168 if Guid::parse(&draft.global_id).is_none() {
169 return Err(StructuralError::InvalidGlobalId);
170 }
171 let entity_type = match draft.kind {
172 LoadGroupKind::Group { .. } => "IfcStructuralLoadGroup",
173 LoadGroupKind::Case { .. } => "IfcStructuralLoadCase",
174 };
175 let predefined_type = match draft.kind {
178 LoadGroupKind::Group { predefined_type } => {
179 if predefined_type.eq_ignore_ascii_case("LOAD_CASE") {
180 return Err(StructuralError::SemanticViolation {
181 entity: None,
182 rule: "LOAD_CASE PredefinedType requires IfcStructuralLoadCase",
183 });
184 }
185 predefined_type
186 }
187 LoadGroupKind::Case { .. } => "LOAD_CASE",
188 };
189
190 for (attribute, token) in [
191 ("PredefinedType", predefined_type),
192 ("ActionType", draft.action_type),
193 ("ActionSource", draft.action_source),
194 ] {
195 validate_enum_token(schema, entity_type, attribute, token)?;
196 }
197
198 let user_defined = [predefined_type, draft.action_type, draft.action_source]
202 .iter()
203 .any(|token| token.eq_ignore_ascii_case("USERDEFINED"));
204 if user_defined
205 && draft
206 .object_type
207 .as_deref()
208 .is_none_or(|value| value.trim().is_empty())
209 {
210 return Err(StructuralError::SemanticViolation {
211 entity: None,
212 rule: "USERDEFINED load group requires an ObjectType",
213 });
214 }
215
216 validate_finite(draft.coefficient, entity_type, "Coefficient")?;
217 let self_weight = match draft.kind {
218 LoadGroupKind::Case {
219 self_weight_coefficients: Some(ratios),
220 } => {
221 for ratio in ratios {
222 validate_finite(Some(ratio), entity_type, "SelfWeightCoefficients")?;
223 }
224 Some(Value::List(
225 ratios.iter().copied().map(Value::Real).collect(),
226 ))
227 }
228 _ => None,
229 };
230
231 validate_root_refs(tx, model, schema, draft.owner_history)?;
232
233 let mut fields = vec![
234 ("GlobalId", Value::Text(draft.global_id.into())),
235 ("Name", optional_text(draft.name)),
236 ("Description", optional_text(draft.description)),
237 ("ObjectType", optional_text(draft.object_type)),
238 ("PredefinedType", Value::Enum(predefined_type.into())),
239 ("ActionType", Value::Enum(draft.action_type.into())),
240 ("ActionSource", Value::Enum(draft.action_source.into())),
241 ("Purpose", optional_text(draft.purpose)),
242 (
243 "Coefficient",
244 draft.coefficient.map_or(Value::Null, Value::Real),
245 ),
246 ];
247 if let Some(owner_history) = draft.owner_history {
248 fields.push(("OwnerHistory", Value::Ref(owner_history)));
249 }
250 if let Some(values) = self_weight {
254 fields.push(("SelfWeightCoefficients", values));
255 }
256 Ok(tx.create(build_named(schema, entity_type, fields)?))
257}
258
259pub(super) fn validate_enum_token(
264 schema: &Schema,
265 entity_type: &'static str,
266 attribute: &'static str,
267 token: &str,
268) -> StructuralResult<()> {
269 let declared = schema
270 .attributes(entity_type)
271 .iter()
272 .find(|candidate| candidate.name.eq_ignore_ascii_case(attribute))
273 .and_then(|candidate| schema.type_def(&candidate.type_name))
274 .is_some_and(|definition| match &definition.kind {
275 TypeKind::Enumeration(values) => values.iter().any(|member| member == token),
276 _ => false,
277 });
278 if declared {
279 return Ok(());
280 }
281 Err(StructuralError::InvalidDraftValue {
282 entity_type,
283 attribute,
284 expected: "a token the schema declares for this attribute",
285 })
286}
287
288fn validate_finite(
293 value: Option<f64>,
294 entity_type: &'static str,
295 attribute: &'static str,
296) -> StructuralResult<()> {
297 if value.is_some_and(|number| !number.is_finite()) {
298 return Err(StructuralError::InvalidDraftValue {
299 entity_type,
300 attribute,
301 expected: "a finite ratio",
302 });
303 }
304 Ok(())
305}
306
307fn validate_root_refs(
309 tx: &Transaction,
310 model: &Model,
311 schema: &Schema,
312 owner_history: Option<EntityId>,
313) -> StructuralResult<()> {
314 validate_optional_ref(tx, model, schema, owner_history, "IfcOwnerHistory")
315}