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)]
10#[non_exhaustive]
11pub struct StructuralRootDraft {
12 pub global_id: String,
14 pub owner_history: Option<EntityId>,
16 pub name: Option<String>,
18 pub description: Option<String>,
20 pub object_type: Option<String>,
22 pub object_placement: Option<EntityId>,
24 pub representation: Option<EntityId>,
26}
27
28impl StructuralRootDraft {
29 #[must_use]
31 pub fn new(global_id: impl Into<String>) -> Self {
32 Self {
33 global_id: global_id.into(),
34 owner_history: None,
35 name: None,
36 description: None,
37 object_type: None,
38 object_placement: None,
39 representation: None,
40 }
41 }
42
43 #[must_use]
46 pub fn owner_history(mut self, value: EntityId) -> Self {
47 self.owner_history = Some(value);
48 self
49 }
50
51 #[must_use]
53 pub fn name(mut self, value: impl Into<String>) -> Self {
54 self.name = Some(value.into());
55 self
56 }
57
58 #[must_use]
60 pub fn description(mut self, value: impl Into<String>) -> Self {
61 self.description = Some(value.into());
62 self
63 }
64
65 #[must_use]
68 pub fn object_type(mut self, value: impl Into<String>) -> Self {
69 self.object_type = Some(value.into());
70 self
71 }
72
73 #[must_use]
76 pub fn object_placement(mut self, value: EntityId) -> Self {
77 self.object_placement = Some(value);
78 self
79 }
80
81 #[must_use]
84 pub fn representation(mut self, value: EntityId) -> Self {
85 self.representation = Some(value);
86 self
87 }
88}
89
90impl Default for StructuralRootDraft {
91 fn default() -> Self {
92 Self {
93 global_id: "0000000000000000000000".into(),
94 owner_history: None,
95 name: None,
96 description: None,
97 object_type: None,
98 object_placement: None,
99 representation: None,
100 }
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum MemberPredefinedType {
107 RigidJoinedMember,
109 PinJoinedMember,
111 Cable,
113 TensionMember,
115 CompressionMember,
117 BendingElement,
119 MembraneElement,
121 Shell,
123 UserDefined,
125 NotDefined,
127}
128
129impl MemberPredefinedType {
130 fn token(self) -> &'static str {
131 match self {
132 Self::RigidJoinedMember => "RIGID_JOINED_MEMBER",
133 Self::PinJoinedMember => "PIN_JOINED_MEMBER",
134 Self::Cable => "CABLE",
135 Self::TensionMember => "TENSION_MEMBER",
136 Self::CompressionMember => "COMPRESSION_MEMBER",
137 Self::BendingElement => "BENDING_ELEMENT",
138 Self::MembraneElement => "MEMBRANE_ELEMENT",
139 Self::Shell => "SHELL",
140 Self::UserDefined => "USERDEFINED",
141 Self::NotDefined => "NOTDEFINED",
142 }
143 }
144}
145
146#[derive(Debug, Clone)]
148pub enum MemberDraftKind {
149 Curve {
151 predefined_type: MemberPredefinedType,
153 axis: Option<EntityId>,
155 varying: bool,
162 },
163 Surface {
165 predefined_type: MemberPredefinedType,
167 thickness: Option<f64>,
169 varying: bool,
175 },
176}
177
178#[derive(Debug, Clone)]
180#[non_exhaustive]
181pub struct MemberDraft {
182 pub root: StructuralRootDraft,
184 pub kind: MemberDraftKind,
186}
187
188impl MemberDraft {
189 #[must_use]
191 pub fn new(root: StructuralRootDraft, kind: MemberDraftKind) -> Self {
192 Self { root, kind }
193 }
194}
195
196#[derive(Debug, Clone)]
198pub enum ConnectionDraftKind {
199 Point {
201 applied_condition: Option<EntityId>,
203 condition_coordinate_system: Option<EntityId>,
205 },
206 Curve {
208 applied_condition: Option<EntityId>,
210 axis: Option<EntityId>,
212 },
213 Surface {
215 applied_condition: Option<EntityId>,
217 },
218}
219
220#[derive(Debug, Clone)]
222#[non_exhaustive]
223pub struct ConnectionDraft {
224 pub root: StructuralRootDraft,
226 pub kind: ConnectionDraftKind,
228}
229
230impl ConnectionDraft {
231 #[must_use]
233 pub fn new(root: StructuralRootDraft, kind: ConnectionDraftKind) -> Self {
234 Self { root, kind }
235 }
236}
237
238pub(super) fn validate_root(
239 tx: &Transaction,
240 model: &Model,
241 schema: &Schema,
242 root: &StructuralRootDraft,
243) -> StructuralResult<()> {
244 if Guid::parse(&root.global_id).is_none() {
245 return Err(StructuralError::InvalidGlobalId);
246 }
247 validate_optional_ref(tx, model, schema, root.owner_history, "IfcOwnerHistory")?;
248 validate_optional_ref(
249 tx,
250 model,
251 schema,
252 root.object_placement,
253 "IfcObjectPlacement",
254 )?;
255 validate_optional_ref(
256 tx,
257 model,
258 schema,
259 root.representation,
260 "IfcProductRepresentation",
261 )?;
262 Ok(())
263}
264
265pub(super) fn root_fields(root: StructuralRootDraft) -> Vec<(&'static str, Value)> {
266 vec![
267 ("GlobalId", Value::Text(root.global_id.into())),
268 ("OwnerHistory", optional_ref(root.owner_history)),
269 ("Name", optional_text(root.name)),
270 ("Description", optional_text(root.description)),
271 ("ObjectType", optional_text(root.object_type)),
272 ("ObjectPlacement", optional_ref(root.object_placement)),
273 ("Representation", optional_ref(root.representation)),
274 ]
275}
276
277pub fn stage_member(
289 tx: &mut Transaction,
290 model: &Model,
291 schema: &Schema,
292 draft: MemberDraft,
293) -> StructuralResult<EntityId> {
294 validate_root(tx, model, schema, &draft.root)?;
295 let (entity_type, predefined_type, axis, thickness, curve) = match draft.kind {
296 MemberDraftKind::Curve {
297 predefined_type,
298 axis,
299 varying,
300 } => (
301 if varying {
302 "IfcStructuralCurveMemberVarying"
303 } else {
304 "IfcStructuralCurveMember"
305 },
306 predefined_type,
307 axis,
308 None,
309 true,
310 ),
311 MemberDraftKind::Surface {
312 predefined_type,
313 thickness,
314 varying,
315 } => (
316 if varying {
317 "IfcStructuralSurfaceMemberVarying"
318 } else {
319 "IfcStructuralSurfaceMember"
320 },
321 predefined_type,
322 None,
323 thickness,
324 false,
325 ),
326 };
327 let token = predefined_type.token();
328 let valid = if curve {
329 matches!(
330 predefined_type,
331 MemberPredefinedType::RigidJoinedMember
332 | MemberPredefinedType::PinJoinedMember
333 | MemberPredefinedType::Cable
334 | MemberPredefinedType::TensionMember
335 | MemberPredefinedType::CompressionMember
336 | MemberPredefinedType::UserDefined
337 | MemberPredefinedType::NotDefined
338 )
339 } else {
340 matches!(
341 predefined_type,
342 MemberPredefinedType::BendingElement
343 | MemberPredefinedType::MembraneElement
344 | MemberPredefinedType::Shell
345 | MemberPredefinedType::UserDefined
346 | MemberPredefinedType::NotDefined
347 )
348 };
349 if !valid {
350 return Err(StructuralError::InvalidDraftValue {
351 entity_type,
352 attribute: "PredefinedType",
353 expected: "member-kind enum value",
354 });
355 }
356 if predefined_type == MemberPredefinedType::UserDefined
357 && draft.root.object_type.as_deref().is_none_or(str::is_empty)
358 {
359 return Err(StructuralError::SemanticViolation {
360 entity: None,
361 rule: "USERDEFINED structural member requires ObjectType",
362 });
363 }
364 let has_axis = schema
365 .attributes(entity_type)
366 .iter()
367 .any(|attribute| attribute.name.eq_ignore_ascii_case("Axis"));
368 let validated_axis = if has_axis {
369 let target = axis.ok_or(StructuralError::MissingRequired {
370 entity_type: entity_type.into(),
371 attribute: "Axis".into(),
372 })?;
373 validate_ref(tx, model, schema, target, "IfcDirection")?;
374 Some(target)
375 } else if axis.is_some() {
376 return Err(StructuralError::UnsupportedAttribute {
377 entity_type: entity_type.into(),
378 attribute: "Axis".into(),
379 });
380 } else {
381 None
382 };
383 if thickness.is_some_and(|value| !value.is_finite() || value <= 0.0) {
384 return Err(StructuralError::InvalidDraftValue {
385 entity_type,
386 attribute: "Thickness",
387 expected: "positive finite thickness or null",
388 });
389 }
390 if predefined_type == MemberPredefinedType::Shell && thickness.is_none() {
391 return Err(StructuralError::InvalidDraftValue {
392 entity_type,
393 attribute: "Thickness",
394 expected: "SHELL requires positive finite Thickness",
395 });
396 }
397 let mut fields = root_fields(draft.root);
398 fields.push(("PredefinedType", Value::Enum(token.into())));
399 if let Some(axis) = validated_axis {
400 fields.push(("Axis", Value::Ref(axis)));
401 }
402 if schema
403 .attributes(entity_type)
404 .iter()
405 .any(|a| a.name.eq_ignore_ascii_case("Thickness"))
406 {
407 fields.push(("Thickness", thickness.map_or(Value::Null, Value::Real)));
408 }
409 Ok(tx.create(build_named(schema, entity_type, fields)?))
410}
411
412pub fn stage_connection(
419 tx: &mut Transaction,
420 model: &Model,
421 schema: &Schema,
422 draft: ConnectionDraft,
423) -> StructuralResult<EntityId> {
424 validate_root(tx, model, schema, &draft.root)?;
425 let (entity_type, applied_condition, axis, coordinate_system) = match draft.kind {
426 ConnectionDraftKind::Point {
427 applied_condition,
428 condition_coordinate_system,
429 } => (
430 "IfcStructuralPointConnection",
431 applied_condition,
432 None,
433 condition_coordinate_system,
434 ),
435 ConnectionDraftKind::Curve {
436 applied_condition,
437 axis,
438 } => (
439 "IfcStructuralCurveConnection",
440 applied_condition,
441 axis,
442 None,
443 ),
444 ConnectionDraftKind::Surface { applied_condition } => (
445 "IfcStructuralSurfaceConnection",
446 applied_condition,
447 None,
448 None,
449 ),
450 };
451 validate_optional_ref(tx, model, schema, applied_condition, "IfcBoundaryCondition")?;
452 validate_optional_ref(tx, model, schema, coordinate_system, "IfcAxis2Placement3D")?;
453 let axis_attribute = ["Axis", "AxisDirection"].into_iter().find(|candidate| {
458 schema
459 .attributes(entity_type)
460 .iter()
461 .any(|attribute| attribute.name.eq_ignore_ascii_case(candidate))
462 });
463 let validated_axis = if let Some(axis_attribute) = axis_attribute {
464 let target = axis.ok_or_else(|| StructuralError::MissingRequired {
465 entity_type: entity_type.into(),
466 attribute: axis_attribute.into(),
467 })?;
468 validate_ref(tx, model, schema, target, "IfcDirection")?;
469 Some((axis_attribute, target))
470 } else if axis.is_some() {
471 return Err(StructuralError::UnsupportedAttribute {
472 entity_type: entity_type.into(),
473 attribute: "Axis".into(),
474 });
475 } else {
476 None
477 };
478 let mut fields = root_fields(draft.root);
479 fields.push(("AppliedCondition", optional_ref(applied_condition)));
480 if let Some((axis_attribute, axis)) = validated_axis {
481 fields.push((axis_attribute, Value::Ref(axis)));
482 }
483 if schema
484 .attributes(entity_type)
485 .iter()
486 .any(|a| a.name.eq_ignore_ascii_case("ConditionCoordinateSystem"))
487 {
488 fields.push(("ConditionCoordinateSystem", optional_ref(coordinate_system)));
489 }
490 Ok(tx.create(build_named(schema, entity_type, fields)?))
491}