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