1use ifc_model::guid::Guid;
2use ifc_model::{EntityId, Model, Transaction, Value};
3use ifc_schema::Schema;
4
5use super::{build_named, optional_ref, optional_text, validate_optional_ref, validate_ref};
6use crate::error::{StructuralError, StructuralResult};
7
8#[derive(Debug, Clone)]
10pub struct StructuralRootDraft {
11 pub global_id: String,
13 pub owner_history: Option<EntityId>,
15 pub name: Option<String>,
17 pub description: Option<String>,
19 pub object_type: Option<String>,
21 pub object_placement: Option<EntityId>,
23 pub representation: Option<EntityId>,
25}
26
27impl Default for StructuralRootDraft {
28 fn default() -> Self {
29 Self {
30 global_id: "0000000000000000000000".into(),
31 owner_history: None,
32 name: None,
33 description: None,
34 object_type: None,
35 object_placement: None,
36 representation: None,
37 }
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum MemberPredefinedType {
44 RigidJoinedMember,
46 PinJoinedMember,
48 Cable,
50 TensionMember,
52 CompressionMember,
54 BendingElement,
56 MembraneElement,
58 Shell,
60 UserDefined,
62 NotDefined,
64}
65
66impl MemberPredefinedType {
67 fn token(self) -> &'static str {
68 match self {
69 Self::RigidJoinedMember => "RIGID_JOINED_MEMBER",
70 Self::PinJoinedMember => "PIN_JOINED_MEMBER",
71 Self::Cable => "CABLE",
72 Self::TensionMember => "TENSION_MEMBER",
73 Self::CompressionMember => "COMPRESSION_MEMBER",
74 Self::BendingElement => "BENDING_ELEMENT",
75 Self::MembraneElement => "MEMBRANE_ELEMENT",
76 Self::Shell => "SHELL",
77 Self::UserDefined => "USERDEFINED",
78 Self::NotDefined => "NOTDEFINED",
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
85pub enum MemberDraftKind {
86 Curve {
88 predefined_type: MemberPredefinedType,
90 axis: Option<EntityId>,
92 varying: bool,
99 },
100 Surface {
102 predefined_type: MemberPredefinedType,
104 thickness: Option<f64>,
106 varying: bool,
112 },
113}
114
115#[derive(Debug, Clone)]
117pub struct MemberDraft {
118 pub root: StructuralRootDraft,
120 pub kind: MemberDraftKind,
122}
123
124#[derive(Debug, Clone)]
126pub enum ConnectionDraftKind {
127 Point {
129 applied_condition: Option<EntityId>,
131 condition_coordinate_system: Option<EntityId>,
133 },
134 Curve {
136 applied_condition: Option<EntityId>,
138 axis: Option<EntityId>,
140 },
141 Surface {
143 applied_condition: Option<EntityId>,
145 },
146}
147
148#[derive(Debug, Clone)]
150pub struct ConnectionDraft {
151 pub root: StructuralRootDraft,
153 pub kind: ConnectionDraftKind,
155}
156
157pub(super) fn validate_root(
158 tx: &Transaction,
159 model: &Model,
160 schema: &Schema,
161 root: &StructuralRootDraft,
162) -> StructuralResult<()> {
163 if Guid::parse(&root.global_id).is_none() {
164 return Err(StructuralError::InvalidGlobalId);
165 }
166 validate_optional_ref(tx, model, schema, root.owner_history, "IfcOwnerHistory")?;
167 validate_optional_ref(
168 tx,
169 model,
170 schema,
171 root.object_placement,
172 "IfcObjectPlacement",
173 )?;
174 validate_optional_ref(
175 tx,
176 model,
177 schema,
178 root.representation,
179 "IfcProductRepresentation",
180 )?;
181 Ok(())
182}
183
184pub(super) fn root_fields(root: StructuralRootDraft) -> Vec<(&'static str, Value)> {
185 vec![
186 ("GlobalId", Value::Text(root.global_id.into())),
187 ("OwnerHistory", optional_ref(root.owner_history)),
188 ("Name", optional_text(root.name)),
189 ("Description", optional_text(root.description)),
190 ("ObjectType", optional_text(root.object_type)),
191 ("ObjectPlacement", optional_ref(root.object_placement)),
192 ("Representation", optional_ref(root.representation)),
193 ]
194}
195
196pub fn stage_member(
208 tx: &mut Transaction,
209 model: &Model,
210 schema: &Schema,
211 draft: MemberDraft,
212) -> StructuralResult<EntityId> {
213 validate_root(tx, model, schema, &draft.root)?;
214 let (entity_type, predefined_type, axis, thickness, curve) = match draft.kind {
215 MemberDraftKind::Curve {
216 predefined_type,
217 axis,
218 varying,
219 } => (
220 if varying {
221 "IfcStructuralCurveMemberVarying"
222 } else {
223 "IfcStructuralCurveMember"
224 },
225 predefined_type,
226 axis,
227 None,
228 true,
229 ),
230 MemberDraftKind::Surface {
231 predefined_type,
232 thickness,
233 varying,
234 } => (
235 if varying {
236 "IfcStructuralSurfaceMemberVarying"
237 } else {
238 "IfcStructuralSurfaceMember"
239 },
240 predefined_type,
241 None,
242 thickness,
243 false,
244 ),
245 };
246 let token = predefined_type.token();
247 let valid = if curve {
248 matches!(
249 predefined_type,
250 MemberPredefinedType::RigidJoinedMember
251 | MemberPredefinedType::PinJoinedMember
252 | MemberPredefinedType::Cable
253 | MemberPredefinedType::TensionMember
254 | MemberPredefinedType::CompressionMember
255 | MemberPredefinedType::UserDefined
256 | MemberPredefinedType::NotDefined
257 )
258 } else {
259 matches!(
260 predefined_type,
261 MemberPredefinedType::BendingElement
262 | MemberPredefinedType::MembraneElement
263 | MemberPredefinedType::Shell
264 | MemberPredefinedType::UserDefined
265 | MemberPredefinedType::NotDefined
266 )
267 };
268 if !valid {
269 return Err(StructuralError::InvalidDraftValue {
270 entity_type,
271 attribute: "PredefinedType",
272 expected: "member-kind enum value",
273 });
274 }
275 if predefined_type == MemberPredefinedType::UserDefined
276 && draft.root.object_type.as_deref().is_none_or(str::is_empty)
277 {
278 return Err(StructuralError::SemanticViolation {
279 entity: None,
280 rule: "USERDEFINED structural member requires ObjectType",
281 });
282 }
283 let has_axis = schema
284 .attributes(entity_type)
285 .iter()
286 .any(|attribute| attribute.name.eq_ignore_ascii_case("Axis"));
287 let validated_axis = if has_axis {
288 let target = axis.ok_or(StructuralError::MissingRequired {
289 entity_type: entity_type.into(),
290 attribute: "Axis".into(),
291 })?;
292 validate_ref(tx, model, schema, target, "IfcDirection")?;
293 Some(target)
294 } else if axis.is_some() {
295 return Err(StructuralError::UnsupportedAttribute {
296 entity_type: entity_type.into(),
297 attribute: "Axis".into(),
298 });
299 } else {
300 None
301 };
302 if thickness.is_some_and(|value| !value.is_finite() || value <= 0.0) {
303 return Err(StructuralError::InvalidDraftValue {
304 entity_type,
305 attribute: "Thickness",
306 expected: "positive finite thickness or null",
307 });
308 }
309 if predefined_type == MemberPredefinedType::Shell && thickness.is_none() {
310 return Err(StructuralError::InvalidDraftValue {
311 entity_type,
312 attribute: "Thickness",
313 expected: "SHELL requires positive finite Thickness",
314 });
315 }
316 let mut fields = root_fields(draft.root);
317 fields.push(("PredefinedType", Value::Enum(token.into())));
318 if let Some(axis) = validated_axis {
319 fields.push(("Axis", Value::Ref(axis)));
320 }
321 if schema
322 .attributes(entity_type)
323 .iter()
324 .any(|a| a.name.eq_ignore_ascii_case("Thickness"))
325 {
326 fields.push(("Thickness", thickness.map_or(Value::Null, Value::Real)));
327 }
328 Ok(tx.create(build_named(schema, entity_type, fields)?))
329}
330
331pub fn stage_connection(
338 tx: &mut Transaction,
339 model: &Model,
340 schema: &Schema,
341 draft: ConnectionDraft,
342) -> StructuralResult<EntityId> {
343 validate_root(tx, model, schema, &draft.root)?;
344 let (entity_type, applied_condition, axis, coordinate_system) = match draft.kind {
345 ConnectionDraftKind::Point {
346 applied_condition,
347 condition_coordinate_system,
348 } => (
349 "IfcStructuralPointConnection",
350 applied_condition,
351 None,
352 condition_coordinate_system,
353 ),
354 ConnectionDraftKind::Curve {
355 applied_condition,
356 axis,
357 } => (
358 "IfcStructuralCurveConnection",
359 applied_condition,
360 axis,
361 None,
362 ),
363 ConnectionDraftKind::Surface { applied_condition } => (
364 "IfcStructuralSurfaceConnection",
365 applied_condition,
366 None,
367 None,
368 ),
369 };
370 validate_optional_ref(tx, model, schema, applied_condition, "IfcBoundaryCondition")?;
371 validate_optional_ref(tx, model, schema, coordinate_system, "IfcAxis2Placement3D")?;
372 let axis_attribute = ["Axis", "AxisDirection"].into_iter().find(|candidate| {
377 schema
378 .attributes(entity_type)
379 .iter()
380 .any(|attribute| attribute.name.eq_ignore_ascii_case(candidate))
381 });
382 let validated_axis = if let Some(axis_attribute) = axis_attribute {
383 let target = axis.ok_or_else(|| StructuralError::MissingRequired {
384 entity_type: entity_type.into(),
385 attribute: axis_attribute.into(),
386 })?;
387 validate_ref(tx, model, schema, target, "IfcDirection")?;
388 Some((axis_attribute, target))
389 } else if axis.is_some() {
390 return Err(StructuralError::UnsupportedAttribute {
391 entity_type: entity_type.into(),
392 attribute: "Axis".into(),
393 });
394 } else {
395 None
396 };
397 let mut fields = root_fields(draft.root);
398 fields.push(("AppliedCondition", optional_ref(applied_condition)));
399 if let Some((axis_attribute, axis)) = validated_axis {
400 fields.push((axis_attribute, Value::Ref(axis)));
401 }
402 if schema
403 .attributes(entity_type)
404 .iter()
405 .any(|a| a.name.eq_ignore_ascii_case("ConditionCoordinateSystem"))
406 {
407 fields.push(("ConditionCoordinateSystem", optional_ref(coordinate_system)));
408 }
409 Ok(tx.create(build_named(schema, entity_type, fields)?))
410}