1use std::collections::HashSet;
4use std::sync::Arc;
5
6use ifc_model::guid::Guid;
7use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
8use ifc_schema::Schema;
9
10use crate::error::{StructuralError, StructuralResult};
11use crate::AnalysisModelType;
12
13mod action;
14mod condition;
15mod item;
16mod load_group;
17mod reaction;
18mod relation;
19mod stiffness;
20
21pub use action::{stage_action, ActionDraft, ActionDraftKind, ProjectedOrTrue};
22pub use condition::{
23 stage_boundary_condition, stage_boundary_condition_in, stage_connection_condition,
24 BoundaryConditionDraft, ConnectionConditionDraft,
25};
26pub use item::{
27 stage_connection, stage_member, ConnectionDraft, ConnectionDraftKind, MemberDraft,
28 MemberDraftKind, MemberPredefinedType, StructuralRootDraft,
29};
30pub use load_group::{stage_load_group, LoadGroupDraft, LoadGroupKind};
31pub use reaction::{
32 stage_reaction, stage_result_group, ReactionDraft, ReactionDraftKind, ResultGroupDraft,
33};
34pub use relation::{
35 stage_activity_assignment, stage_member_connection, ActivityAssignmentDraft,
36 MemberConnectionDraft, RelationshipRootDraft,
37};
38
39#[derive(Debug, Clone)]
41pub struct AnalysisModelDraft {
42 pub global_id: String,
44 pub owner_history: Option<EntityId>,
46 pub name: Option<String>,
48 pub description: Option<String>,
50 pub object_type: Option<String>,
52 pub predefined_type: AnalysisModelType,
54 pub orientation_of_2d_plane: Option<EntityId>,
56 pub loaded_by: Vec<EntityId>,
58 pub result_groups: Vec<EntityId>,
60 pub shared_placement: Option<EntityId>,
62}
63
64impl Default for AnalysisModelDraft {
65 fn default() -> Self {
66 Self {
67 global_id: "0000000000000000000000".to_owned(),
68 owner_history: None,
69 name: None,
70 description: None,
71 object_type: None,
72 predefined_type: AnalysisModelType::NotDefined,
73 orientation_of_2d_plane: None,
74 loaded_by: Vec::new(),
75 result_groups: Vec::new(),
76 shared_placement: None,
77 }
78 }
79}
80
81#[derive(Debug, Clone, PartialEq)]
83#[non_exhaustive]
84pub enum LoadDraft {
85 SingleForce {
87 name: Option<String>,
89 force: [Option<f64>; 3],
91 moment: [Option<f64>; 3],
93 },
94 LinearForce {
96 name: Option<String>,
98 force: [Option<f64>; 3],
100 moment: [Option<f64>; 3],
102 },
103 PlanarForce {
105 name: Option<String>,
107 force: [Option<f64>; 3],
109 },
110 Temperature {
112 name: Option<String>,
114 delta: [Option<f64>; 3],
117 },
118 SingleForceWarping {
123 name: Option<String>,
125 force: [Option<f64>; 3],
127 moment: [Option<f64>; 3],
129 warping_moment: Option<f64>,
131 },
132 SingleDisplacement {
138 name: Option<String>,
140 displacement: [Option<f64>; 3],
142 rotation: [Option<f64>; 3],
144 },
145 SingleDisplacementDistortion {
150 name: Option<String>,
152 displacement: [Option<f64>; 3],
154 rotation: [Option<f64>; 3],
156 distortion: Option<f64>,
158 },
159 SurfaceReinforcementArea {
165 name: Option<String>,
167 surface_1: Option<Vec<f64>>,
169 surface_2: Option<Vec<f64>>,
171 shear: Option<f64>,
173 },
174 Configuration {
180 name: Option<String>,
182 values: Vec<EntityId>,
184 locations: Option<Vec<[f64; 2]>>,
186 },
187}
188
189pub fn stage_analysis_model(
199 tx: &mut Transaction,
200 model: &Model,
201 schema: &Schema,
202 draft: AnalysisModelDraft,
203) -> StructuralResult<EntityId> {
204 if Guid::parse(&draft.global_id).is_none() {
205 return Err(StructuralError::InvalidGlobalId);
206 }
207 if draft.predefined_type == AnalysisModelType::UserDefined
208 && draft
209 .object_type
210 .as_deref()
211 .is_none_or(|value| value.trim().is_empty())
212 {
213 return Err(StructuralError::SemanticViolation {
214 entity: None,
215 rule: "USERDEFINED analysis model requires ObjectType",
216 });
217 }
218
219 validate_unique_set_members(&draft.loaded_by, "IfcStructuralAnalysisModel", "LoadedBy")?;
220 validate_unique_set_members(
221 &draft.result_groups,
222 "IfcStructuralAnalysisModel",
223 "HasResults",
224 )?;
225 validate_optional_ref(tx, model, schema, draft.owner_history, "IfcOwnerHistory")?;
226 validate_optional_ref(
227 tx,
228 model,
229 schema,
230 draft.orientation_of_2d_plane,
231 "IfcAxis2Placement3D",
232 )?;
233 validate_refs(
234 tx,
235 model,
236 schema,
237 &draft.loaded_by,
238 "IfcStructuralLoadGroup",
239 )?;
240 validate_refs(
241 tx,
242 model,
243 schema,
244 &draft.result_groups,
245 "IfcStructuralResultGroup",
246 )?;
247 validate_optional_ref(
248 tx,
249 model,
250 schema,
251 draft.shared_placement,
252 "IfcObjectPlacement",
253 )?;
254
255 let mut fields = vec![
256 ("GlobalId", Value::Text(Arc::from(draft.global_id))),
257 ("Name", optional_text(draft.name)),
258 ("Description", optional_text(draft.description)),
259 ("ObjectType", optional_text(draft.object_type)),
260 (
261 "PredefinedType",
262 Value::Enum(Arc::from(draft.predefined_type.token())),
263 ),
264 (
265 "OrientationOf2DPlane",
266 optional_ref(draft.orientation_of_2d_plane),
267 ),
268 ("LoadedBy", optional_refs(draft.loaded_by)),
269 ("HasResults", optional_refs(draft.result_groups)),
270 ];
271 if let Some(owner_history) = draft.owner_history {
272 fields.push(("OwnerHistory", Value::Ref(owner_history)));
273 }
274 if schema
275 .attribute_names("IfcStructuralAnalysisModel")
276 .iter()
277 .any(|name| name.eq_ignore_ascii_case("SharedPlacement"))
278 {
279 fields.push(("SharedPlacement", optional_ref(draft.shared_placement)));
280 } else if draft.shared_placement.is_some() {
281 return Err(StructuralError::UnsupportedAttribute {
282 entity_type: "IfcStructuralAnalysisModel".to_owned(),
283 attribute: "SharedPlacement".to_owned(),
284 });
285 }
286
287 let entity = build_named(schema, "IfcStructuralAnalysisModel", fields)?;
288 Ok(tx.create(entity))
289}
290
291pub fn stage_load(
296 tx: &mut Transaction,
297 schema: &Schema,
298 draft: LoadDraft,
299) -> StructuralResult<EntityId> {
300 let (entity_type, name, attributes, values): (&str, Option<String>, &[&str], Vec<Option<f64>>) =
304 match draft {
305 LoadDraft::SurfaceReinforcementArea {
306 name,
307 surface_1,
308 surface_2,
309 shear,
310 } => return stage_surface_reinforcement(tx, schema, name, surface_1, surface_2, shear),
311 LoadDraft::Configuration {
312 name,
313 values,
314 locations,
315 } => return stage_load_configuration(tx, schema, name, values, locations),
316 LoadDraft::SingleForce {
317 name,
318 force,
319 moment,
320 } => (
321 "IfcStructuralLoadSingleForce",
322 name,
323 &[
324 "ForceX", "ForceY", "ForceZ", "MomentX", "MomentY", "MomentZ",
325 ],
326 force.into_iter().chain(moment).collect(),
327 ),
328 LoadDraft::LinearForce {
329 name,
330 force,
331 moment,
332 } => (
333 "IfcStructuralLoadLinearForce",
334 name,
335 &[
336 "LinearForceX",
337 "LinearForceY",
338 "LinearForceZ",
339 "LinearMomentX",
340 "LinearMomentY",
341 "LinearMomentZ",
342 ],
343 force.into_iter().chain(moment).collect(),
344 ),
345 LoadDraft::PlanarForce { name, force } => (
346 "IfcStructuralLoadPlanarForce",
347 name,
348 &["PlanarForceX", "PlanarForceY", "PlanarForceZ"],
349 force.to_vec(),
350 ),
351 LoadDraft::Temperature { name, delta } => {
352 let attributes: &[&str] = if schema
353 .attribute_names("IfcStructuralLoadTemperature")
354 .iter()
355 .any(|attribute| attribute.eq_ignore_ascii_case("DeltaTConstant"))
356 {
357 &["DeltaTConstant", "DeltaTY", "DeltaTZ"]
358 } else {
359 &["DeltaT_Constant", "DeltaT_Y", "DeltaT_Z"]
360 };
361 (
362 "IfcStructuralLoadTemperature",
363 name,
364 attributes,
365 delta.to_vec(),
366 )
367 }
368 LoadDraft::SingleForceWarping {
369 name,
370 force,
371 moment,
372 warping_moment,
373 } => (
374 "IfcStructuralLoadSingleForceWarping",
375 name,
376 &[
377 "ForceX",
378 "ForceY",
379 "ForceZ",
380 "MomentX",
381 "MomentY",
382 "MomentZ",
383 "WarpingMoment",
384 ],
385 force
386 .into_iter()
387 .chain(moment)
388 .chain([warping_moment])
389 .collect(),
390 ),
391 LoadDraft::SingleDisplacement {
392 name,
393 displacement,
394 rotation,
395 } => (
396 "IfcStructuralLoadSingleDisplacement",
397 name,
398 &[
399 "DisplacementX",
400 "DisplacementY",
401 "DisplacementZ",
402 "RotationalDisplacementRX",
403 "RotationalDisplacementRY",
404 "RotationalDisplacementRZ",
405 ],
406 displacement.into_iter().chain(rotation).collect(),
407 ),
408 LoadDraft::SingleDisplacementDistortion {
409 name,
410 displacement,
411 rotation,
412 distortion,
413 } => (
414 "IfcStructuralLoadSingleDisplacementDistortion",
415 name,
416 &[
417 "DisplacementX",
418 "DisplacementY",
419 "DisplacementZ",
420 "RotationalDisplacementRX",
421 "RotationalDisplacementRY",
422 "RotationalDisplacementRZ",
423 "Distortion",
424 ],
425 displacement
426 .into_iter()
427 .chain(rotation)
428 .chain([distortion])
429 .collect(),
430 ),
431 };
432 for (attribute, value) in attributes.iter().zip(&values) {
433 if value.is_some_and(|number| !number.is_finite()) {
434 return Err(StructuralError::InvalidDraftValue {
435 entity_type,
436 attribute,
437 expected: "finite load value or null",
438 });
439 }
440 }
441 let mut fields = vec![("Name", optional_text(name))];
442 fields.extend(
443 attributes
444 .iter()
445 .zip(values)
446 .map(|(name, value)| (*name, value.map_or(Value::Null, Value::Real))),
447 );
448 Ok(tx.create(build_named(schema, entity_type, fields)?))
449}
450
451fn stage_surface_reinforcement(
456 tx: &mut Transaction,
457 schema: &Schema,
458 name: Option<String>,
459 surface_1: Option<Vec<f64>>,
460 surface_2: Option<Vec<f64>>,
461 shear: Option<f64>,
462) -> StructuralResult<EntityId> {
463 const ENTITY: &str = "IfcSurfaceReinforcementArea";
464
465 if surface_1.is_none() && surface_2.is_none() && shear.is_none() {
466 return Err(StructuralError::InvalidDraftValue {
467 entity_type: ENTITY,
468 attribute: "SurfaceReinforcement1",
469 expected: "at least one reinforcement area to be specified",
470 });
471 }
472
473 for (attribute, area) in [
474 ("SurfaceReinforcement1", surface_1.as_ref()),
475 ("SurfaceReinforcement2", surface_2.as_ref()),
476 ] {
477 let Some(area) = area else { continue };
478 if !(2..=3).contains(&area.len()) {
479 return Err(StructuralError::InvalidDraftValue {
480 entity_type: ENTITY,
481 attribute,
482 expected: "two or three area values",
483 });
484 }
485 if area.iter().any(|value| !value.is_finite() || *value < 0.0) {
486 return Err(StructuralError::InvalidDraftValue {
487 entity_type: ENTITY,
488 attribute,
489 expected: "finite non-negative area values",
490 });
491 }
492 }
493
494 if shear.is_some_and(|value| !value.is_finite() || value < 0.0) {
495 return Err(StructuralError::InvalidDraftValue {
496 entity_type: ENTITY,
497 attribute: "ShearReinforcement",
498 expected: "a finite non-negative area",
499 });
500 }
501
502 let reals = |values: Option<Vec<f64>>| {
503 values.map_or(Value::Null, |values| {
504 Value::List(values.into_iter().map(Value::Real).collect())
505 })
506 };
507 let fields = vec![
508 ("Name", optional_text(name)),
509 ("SurfaceReinforcement1", reals(surface_1)),
510 ("SurfaceReinforcement2", reals(surface_2)),
511 ("ShearReinforcement", shear.map_or(Value::Null, Value::Real)),
512 ];
513 Ok(tx.create(build_named(schema, ENTITY, fields)?))
514}
515
516fn stage_load_configuration(
522 tx: &mut Transaction,
523 schema: &Schema,
524 name: Option<String>,
525 values: Vec<EntityId>,
526 locations: Option<Vec<[f64; 2]>>,
527) -> StructuralResult<EntityId> {
528 const ENTITY: &str = "IfcStructuralLoadConfiguration";
529
530 if values.is_empty() {
531 return Err(StructuralError::InvalidDraftValue {
532 entity_type: ENTITY,
533 attribute: "Values",
534 expected: "at least one load",
535 });
536 }
537 if let Some(locations) = locations.as_ref() {
538 if locations.len() != values.len() {
539 return Err(StructuralError::InvalidDraftValue {
540 entity_type: ENTITY,
541 attribute: "Locations",
542 expected: "one location per value",
543 });
544 }
545 if locations
546 .iter()
547 .any(|pair| pair.iter().any(|value| !value.is_finite()))
548 {
549 return Err(StructuralError::InvalidDraftValue {
550 entity_type: ENTITY,
551 attribute: "Locations",
552 expected: "finite coordinates",
553 });
554 }
555 }
556
557 let fields = vec![
558 ("Name", optional_text(name)),
559 (
560 "Values",
561 Value::List(values.into_iter().map(Value::Ref).collect()),
562 ),
563 (
564 "Locations",
565 locations.map_or(Value::Null, |locations| {
566 Value::List(
567 locations
568 .into_iter()
569 .map(|pair| Value::List(pair.into_iter().map(Value::Real).collect()))
570 .collect(),
571 )
572 }),
573 ),
574 ];
575 Ok(tx.create(build_named(schema, ENTITY, fields)?))
576}
577
578pub(super) fn build_named(
579 schema: &Schema,
580 entity_type: &str,
581 fields: Vec<(&str, Value)>,
582) -> StructuralResult<Entity> {
583 let attributes = schema.attributes(entity_type);
584 if attributes.is_empty() {
585 return Err(StructuralError::UnsupportedSchema {
586 token: schema.name().to_owned(),
587 });
588 }
589 let mut values = vec![Value::Null; attributes.len()];
590 for (name, value) in fields {
591 let slot = attributes
592 .iter()
593 .position(|attribute| attribute.name.eq_ignore_ascii_case(name))
594 .ok_or_else(|| StructuralError::UnsupportedAttribute {
595 entity_type: entity_type.to_owned(),
596 attribute: name.to_owned(),
597 })?;
598 values[slot] = value;
599 }
600 for (attribute, value) in attributes.iter().zip(&values) {
601 if !attribute.optional && matches!(value, Value::Null) {
602 return Err(StructuralError::MissingRequired {
603 entity_type: entity_type.to_owned(),
604 attribute: attribute.name.clone(),
605 });
606 }
607 }
608 Ok(Entity::new(entity_type.to_ascii_uppercase(), values))
609}
610
611pub(super) fn optional_text(value: Option<String>) -> Value {
612 value.map_or(Value::Null, |value| Value::Text(Arc::from(value)))
613}
614
615pub(super) fn optional_ref(value: Option<EntityId>) -> Value {
616 value.map_or(Value::Null, Value::Ref)
617}
618
619fn optional_refs(values: Vec<EntityId>) -> Value {
620 if values.is_empty() {
621 Value::Null
622 } else {
623 Value::List(values.into_iter().map(Value::Ref).collect())
624 }
625}
626
627pub(super) fn validate_optional_ref(
628 tx: &Transaction,
629 model: &Model,
630 schema: &Schema,
631 target: Option<EntityId>,
632 expected: &'static str,
633) -> StructuralResult<()> {
634 if let Some(target) = target {
635 validate_ref(tx, model, schema, target, expected)?;
636 }
637 Ok(())
638}
639
640fn validate_refs(
641 tx: &Transaction,
642 model: &Model,
643 schema: &Schema,
644 targets: &[EntityId],
645 expected: &'static str,
646) -> StructuralResult<()> {
647 for target in targets {
648 validate_ref(tx, model, schema, *target, expected)?;
649 }
650 Ok(())
651}
652
653fn validate_unique_set_members(
654 targets: &[EntityId],
655 entity_type: &'static str,
656 attribute: &'static str,
657) -> StructuralResult<()> {
658 let mut unique = HashSet::with_capacity(targets.len());
659 if targets.iter().all(|target| unique.insert(*target)) {
660 return Ok(());
661 }
662 Err(StructuralError::InvalidDraftValue {
663 entity_type,
664 attribute,
665 expected: "SET of unique entity references",
666 })
667}
668
669pub(super) fn validate_ref(
670 tx: &Transaction,
671 model: &Model,
672 schema: &Schema,
673 target: EntityId,
674 expected: &'static str,
675) -> StructuralResult<()> {
676 validate_ref_select(tx, model, schema, target, expected, &[expected])
677}
678
679pub(super) fn validate_ref_select(
680 tx: &Transaction,
681 model: &Model,
682 schema: &Schema,
683 target: EntityId,
684 expected: &'static str,
685 members: &[&str],
686) -> StructuralResult<()> {
687 let entity = projected_entity(tx, model, target).ok_or(StructuralError::DanglingReference {
688 entity: EntityId(0),
689 attribute: "draft reference",
690 target,
691 })?;
692 if !members
693 .iter()
694 .any(|member| schema.is_a(&entity.type_name, member))
695 {
696 return Err(StructuralError::WrongReferenceType {
697 entity: EntityId(0),
698 attribute: "draft reference",
699 target,
700 expected,
701 actual: entity.type_name.to_string(),
702 });
703 }
704 Ok(())
705}
706
707pub(super) fn projected_entity(
708 tx: &Transaction,
709 model: &Model,
710 target: EntityId,
711) -> Option<Entity> {
712 let mut projected = model.get(target).cloned();
713 for edit in tx.edits() {
714 match edit {
715 Edit::Create { id, entity } if *id == target => projected = Some(entity.clone()),
716 Edit::SetAttribute { id, slot, value } if *id == target => {
717 let entity = projected.as_mut()?;
718 let attribute = entity.attributes.get_mut(*slot)?;
719 *attribute = value.clone();
720 }
721 Edit::Retype { id, type_name } if *id == target => {
722 projected.as_mut()?.type_name = type_name.clone();
723 }
724 Edit::Remove { id } if *id == target => projected = None,
725 _ => {}
726 }
727 }
728 projected
729}