ifc_structural/authoring/
action.rs1use ifc_model::{EntityId, Model, Transaction, Value};
2use ifc_schema::{Schema, TypeKind};
3
4use super::item::{root_fields, validate_root, StructuralRootDraft};
5use super::{build_named, optional_ref, validate_optional_ref, validate_ref_select};
6use crate::action::CoordinateSystem;
7use crate::error::{StructuralError, StructuralResult};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ProjectedOrTrue {
12 ProjectedLength,
14 TrueLength,
16}
17
18impl ProjectedOrTrue {
19 fn token(self) -> &'static str {
20 match self {
21 Self::ProjectedLength => "PROJECTED_LENGTH",
22 Self::TrueLength => "TRUE_LENGTH",
23 }
24 }
25}
26
27#[derive(Debug, Clone)]
29pub enum ActionDraftKind {
30 Point,
32 Linear {
34 projected_or_true: Option<ProjectedOrTrue>,
36 },
37 Planar {
39 projected_or_true: Option<ProjectedOrTrue>,
41 },
42 Curve {
47 projected_or_true: Option<ProjectedOrTrue>,
49 predefined_type: &'static str,
51 },
52 Surface {
54 projected_or_true: Option<ProjectedOrTrue>,
56 predefined_type: &'static str,
58 },
59}
60
61#[derive(Debug, Clone)]
63pub struct ActionDraft {
64 pub root: StructuralRootDraft,
66 pub applied_load: EntityId,
68 pub coordinate_system: CoordinateSystem,
70 pub destabilizing_load: Option<bool>,
72 pub caused_by: Option<EntityId>,
74 pub kind: ActionDraftKind,
76}
77
78pub fn stage_action(
87 tx: &mut Transaction,
88 model: &Model,
89 schema: &Schema,
90 draft: ActionDraft,
91) -> StructuralResult<EntityId> {
92 validate_root(tx, model, schema, &draft.root)?;
93 let (entity_type, projected_or_true, load_members): (&str, Option<ProjectedOrTrue>, &[&str]) =
94 match draft.kind {
95 ActionDraftKind::Point => (
96 "IfcStructuralPointAction",
97 None,
98 &[
99 "IfcStructuralLoadSingleForce",
100 "IfcStructuralLoadSingleDisplacement",
101 ],
102 ),
103 ActionDraftKind::Linear { projected_or_true } => (
104 "IfcStructuralLinearAction",
105 projected_or_true,
106 &[
107 "IfcStructuralLoadLinearForce",
108 "IfcStructuralLoadTemperature",
109 ],
110 ),
111 ActionDraftKind::Planar { projected_or_true } => (
112 "IfcStructuralPlanarAction",
113 projected_or_true,
114 &[
115 "IfcStructuralLoadPlanarForce",
116 "IfcStructuralLoadTemperature",
117 ],
118 ),
119 ActionDraftKind::Curve {
120 projected_or_true, ..
121 } => (
122 "IfcStructuralCurveAction",
123 projected_or_true,
124 &[
125 "IfcStructuralLoadLinearForce",
126 "IfcStructuralLoadTemperature",
127 ],
128 ),
129 ActionDraftKind::Surface {
130 projected_or_true, ..
131 } => (
132 "IfcStructuralSurfaceAction",
133 projected_or_true,
134 &[
135 "IfcStructuralLoadPlanarForce",
136 "IfcStructuralLoadTemperature",
137 ],
138 ),
139 };
140 validate_ref_select(
141 tx,
142 model,
143 schema,
144 draft.applied_load,
145 "compatible structural load",
146 load_members,
147 )?;
148 validate_optional_ref(tx, model, schema, draft.caused_by, "IfcStructuralReaction")?;
149 if projected_or_true == Some(ProjectedOrTrue::ProjectedLength)
150 && draft.coordinate_system != CoordinateSystem::Global
151 {
152 return Err(StructuralError::SemanticViolation {
153 entity: None,
154 rule: "PROJECTED_LENGTH structural action requires GLOBAL_COORDS",
155 });
156 }
157 let attributes = schema.attributes(entity_type);
158
159 let destabilizing_required = attributes
160 .iter()
161 .find(|a| a.name.eq_ignore_ascii_case("DestabilizingLoad"))
162 .is_some_and(|attribute| !attribute.optional);
163 if destabilizing_required && draft.destabilizing_load.is_none() {
164 return Err(StructuralError::MissingRequired {
165 entity_type: entity_type.into(),
166 attribute: "DestabilizingLoad".into(),
167 });
168 }
169 let has_object_type = draft.root.object_type.is_some();
170 let mut fields = root_fields(draft.root);
171 fields.push(("AppliedLoad", Value::Ref(draft.applied_load)));
172 fields.push((
173 "GlobalOrLocal",
174 Value::Enum(match draft.coordinate_system {
175 CoordinateSystem::Global => "GLOBAL_COORDS".into(),
176 CoordinateSystem::Local => "LOCAL_COORDS".into(),
177 }),
178 ));
179 if attributes
180 .iter()
181 .any(|a| a.name.eq_ignore_ascii_case("DestabilizingLoad"))
182 {
183 fields.push((
184 "DestabilizingLoad",
185 draft.destabilizing_load.map_or(Value::Null, Value::Bool),
186 ));
187 }
188 if attributes
189 .iter()
190 .any(|a| a.name.eq_ignore_ascii_case("CausedBy"))
191 {
192 fields.push(("CausedBy", optional_ref(draft.caused_by)));
193 }
194 if attributes
195 .iter()
196 .any(|a| a.name.eq_ignore_ascii_case("ProjectedOrTrue"))
197 {
198 fields.push((
199 "ProjectedOrTrue",
200 projected_or_true.map_or(Value::Null, |value| Value::Enum(value.token().into())),
201 ));
202 }
203 if attributes
204 .iter()
205 .any(|a| a.name.eq_ignore_ascii_case("PredefinedType"))
206 {
207 let token = match draft.kind {
211 ActionDraftKind::Curve {
212 predefined_type, ..
213 }
214 | ActionDraftKind::Surface {
215 predefined_type, ..
216 } => predefined_type,
217 _ => "NOTDEFINED",
218 };
219 if token.eq_ignore_ascii_case("USERDEFINED") && !has_object_type {
220 return Err(StructuralError::SemanticViolation {
221 entity: None,
222 rule: "USERDEFINED PredefinedType requires an ObjectType",
223 });
224 }
225 if matches!(draft.kind, ActionDraftKind::Curve { .. })
231 && token.eq_ignore_ascii_case("EQUIDISTANT")
232 {
233 return Err(StructuralError::SemanticViolation {
234 entity: None,
235 rule: "IfcStructuralCurveAction.SuitablePredefinedType",
236 });
237 }
238 validate_activity_token(schema, entity_type, token)?;
242 fields.push(("PredefinedType", Value::Enum(token.into())));
243 }
244 Ok(tx.create(build_named(schema, entity_type, fields)?))
245}
246
247pub(super) fn validate_activity_token(
254 schema: &Schema,
255 entity_type: &'static str,
256 token: &str,
257) -> StructuralResult<()> {
258 let declared = schema
259 .attributes(entity_type)
260 .iter()
261 .find(|a| a.name.eq_ignore_ascii_case("PredefinedType"))
262 .and_then(|a| schema.type_def(&a.type_name))
263 .is_some_and(|def| {
264 matches!(&def.kind, TypeKind::Enumeration(values)
265 if values.iter().any(|v| v.eq_ignore_ascii_case(token)))
266 });
267 if declared {
268 return Ok(());
269 }
270 Err(StructuralError::InvalidDraftValue {
271 entity_type,
272 attribute: "PredefinedType",
273 expected: "a token this action's activity enum declares",
274 })
275}