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