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)]
58pub struct LoadGroupDraft {
59 pub global_id: String,
61 pub owner_history: Option<EntityId>,
63 pub name: Option<String>,
65 pub description: Option<String>,
67 pub object_type: Option<String>,
70 pub action_type: &'static str,
72 pub action_source: &'static str,
74 pub coefficient: Option<f64>,
76 pub purpose: Option<String>,
78 pub kind: LoadGroupKind,
80}
81
82pub fn stage_load_group(
93 tx: &mut Transaction,
94 model: &Model,
95 schema: &Schema,
96 draft: LoadGroupDraft,
97) -> StructuralResult<EntityId> {
98 if Guid::parse(&draft.global_id).is_none() {
99 return Err(StructuralError::InvalidGlobalId);
100 }
101 let entity_type = match draft.kind {
102 LoadGroupKind::Group { .. } => "IfcStructuralLoadGroup",
103 LoadGroupKind::Case { .. } => "IfcStructuralLoadCase",
104 };
105 let predefined_type = match draft.kind {
108 LoadGroupKind::Group { predefined_type } => {
109 if predefined_type.eq_ignore_ascii_case("LOAD_CASE") {
110 return Err(StructuralError::SemanticViolation {
111 entity: None,
112 rule: "LOAD_CASE PredefinedType requires IfcStructuralLoadCase",
113 });
114 }
115 predefined_type
116 }
117 LoadGroupKind::Case { .. } => "LOAD_CASE",
118 };
119
120 for (attribute, token) in [
121 ("PredefinedType", predefined_type),
122 ("ActionType", draft.action_type),
123 ("ActionSource", draft.action_source),
124 ] {
125 validate_enum_token(schema, entity_type, attribute, token)?;
126 }
127
128 let user_defined = [predefined_type, draft.action_type, draft.action_source]
132 .iter()
133 .any(|token| token.eq_ignore_ascii_case("USERDEFINED"));
134 if user_defined
135 && draft
136 .object_type
137 .as_deref()
138 .is_none_or(|value| value.trim().is_empty())
139 {
140 return Err(StructuralError::SemanticViolation {
141 entity: None,
142 rule: "USERDEFINED load group requires an ObjectType",
143 });
144 }
145
146 validate_finite(draft.coefficient, entity_type, "Coefficient")?;
147 let self_weight = match draft.kind {
148 LoadGroupKind::Case {
149 self_weight_coefficients: Some(ratios),
150 } => {
151 for ratio in ratios {
152 validate_finite(Some(ratio), entity_type, "SelfWeightCoefficients")?;
153 }
154 Some(Value::List(
155 ratios.iter().copied().map(Value::Real).collect(),
156 ))
157 }
158 _ => None,
159 };
160
161 validate_root_refs(tx, model, schema, draft.owner_history)?;
162
163 let mut fields = vec![
164 ("GlobalId", Value::Text(draft.global_id.into())),
165 ("Name", optional_text(draft.name)),
166 ("Description", optional_text(draft.description)),
167 ("ObjectType", optional_text(draft.object_type)),
168 ("PredefinedType", Value::Enum(predefined_type.into())),
169 ("ActionType", Value::Enum(draft.action_type.into())),
170 ("ActionSource", Value::Enum(draft.action_source.into())),
171 ("Purpose", optional_text(draft.purpose)),
172 (
173 "Coefficient",
174 draft.coefficient.map_or(Value::Null, Value::Real),
175 ),
176 ];
177 if let Some(owner_history) = draft.owner_history {
178 fields.push(("OwnerHistory", Value::Ref(owner_history)));
179 }
180 if let Some(values) = self_weight {
184 fields.push(("SelfWeightCoefficients", values));
185 }
186 Ok(tx.create(build_named(schema, entity_type, fields)?))
187}
188
189pub(super) fn validate_enum_token(
194 schema: &Schema,
195 entity_type: &'static str,
196 attribute: &'static str,
197 token: &str,
198) -> StructuralResult<()> {
199 let declared = schema
200 .attributes(entity_type)
201 .iter()
202 .find(|candidate| candidate.name.eq_ignore_ascii_case(attribute))
203 .and_then(|candidate| schema.type_def(&candidate.type_name))
204 .is_some_and(|definition| match &definition.kind {
205 TypeKind::Enumeration(values) => values.iter().any(|member| member == token),
206 _ => false,
207 });
208 if declared {
209 return Ok(());
210 }
211 Err(StructuralError::InvalidDraftValue {
212 entity_type,
213 attribute,
214 expected: "a token the schema declares for this attribute",
215 })
216}
217
218fn validate_finite(
223 value: Option<f64>,
224 entity_type: &'static str,
225 attribute: &'static str,
226) -> StructuralResult<()> {
227 if value.is_some_and(|number| !number.is_finite()) {
228 return Err(StructuralError::InvalidDraftValue {
229 entity_type,
230 attribute,
231 expected: "a finite ratio",
232 });
233 }
234 Ok(())
235}
236
237fn validate_root_refs(
239 tx: &Transaction,
240 model: &Model,
241 schema: &Schema,
242 owner_history: Option<EntityId>,
243) -> StructuralResult<()> {
244 validate_optional_ref(tx, model, schema, owner_history, "IfcOwnerHistory")
245}