1mod composites;
16mod relationships;
17
18pub use composites::{
19 create_constituent, create_constituent_set, create_profile, create_profile_set,
20 create_profile_set_usage, create_profile_set_usage_tapering, create_profile_with_offsets,
21 ConstituentDraft, ProfileDraft,
22};
23pub use relationships::{
24 create_material_classification_relationship, create_material_definition_representation,
25 create_material_properties, create_material_relationship,
26};
27
28use std::collections::HashSet;
29
30use ifc_model::{Edit, EntityId, Model, Transaction, Value};
31
32use crate::release::Release;
33use crate::{DirectionSense, LayerSetDirection, LogicalValue, MaterialError, MaterialResult};
34
35#[derive(Debug, Clone, Copy)]
37pub struct MaterialDraft<'a> {
38 pub name: &'a str,
40 pub description: Option<&'a str>,
42 pub category: Option<&'a str>,
44}
45
46#[derive(Debug, Clone, Copy)]
48pub struct LayerDraft<'a> {
49 pub material: Option<EntityId>,
51 pub thickness: f64,
54 pub is_ventilated: Option<LogicalValue>,
56 pub name: Option<&'a str>,
58 pub description: Option<&'a str>,
60 pub category: Option<&'a str>,
62 pub priority: Option<i64>,
65}
66
67#[derive(Debug, Clone, Copy)]
69pub struct LayerSetDraft<'a> {
70 pub layers: &'a [EntityId],
72 pub name: Option<&'a str>,
74 pub description: Option<&'a str>,
76}
77
78#[derive(Debug, Clone, Copy)]
80pub struct MaterialAssignmentDraft<'a> {
81 pub global_id: &'a str,
84 pub name: Option<&'a str>,
86 pub description: Option<&'a str>,
88 pub related_objects: &'a [EntityId],
91 pub relating_material: EntityId,
94}
95
96pub fn create_material(
104 tx: &mut Transaction,
105 model: &Model,
106 draft: MaterialDraft<'_>,
107) -> MaterialResult<EntityId> {
108 let record = Release::of(model).record(
109 "IFCMATERIAL",
110 vec![
111 ("Name", text(draft.name)),
112 ("Description", optional_text(draft.description)),
113 ("Category", optional_text(draft.category)),
114 ],
115 )?;
116 Ok(tx.create(record))
117}
118
119fn layer_values(
122 release: Release<'_>,
123 entity: &'static str,
124 draft: &LayerDraft<'_>,
125) -> MaterialResult<Vec<(&'static str, Value)>> {
126 let declared = release.declared(entity, "LayerThickness")?;
127 let positive = declared
128 .type_name
129 .eq_ignore_ascii_case("IfcPositiveLengthMeasure");
130 if !draft.thickness.is_finite() || draft.thickness < 0.0 {
131 return Err(invalid(
132 entity,
133 "LayerThickness",
134 "expected a finite non-negative length",
135 ));
136 }
137 if positive && draft.thickness == 0.0 {
138 return Err(invalid(
139 entity,
140 "LayerThickness",
141 "expected a positive length (IfcPositiveLengthMeasure)",
142 ));
143 }
144 if let Some(priority) = draft.priority.filter(|value| !(0..=100).contains(value)) {
145 return Err(invalid(entity, "Priority", priority.to_string()));
146 }
147 Ok(vec![
148 ("Material", draft.material.map_or(Value::Null, Value::Ref)),
149 ("LayerThickness", Value::Real(draft.thickness)),
150 (
151 "IsVentilated",
152 draft.is_ventilated.map_or(Value::Null, logical),
153 ),
154 ("Name", optional_text(draft.name)),
155 ("Description", optional_text(draft.description)),
156 ("Category", optional_text(draft.category)),
157 (
158 "Priority",
159 draft.priority.map_or(Value::Null, Value::Integer),
160 ),
161 ])
162}
163
164pub fn create_layer(
173 tx: &mut Transaction,
174 model: &Model,
175 draft: LayerDraft<'_>,
176) -> MaterialResult<EntityId> {
177 const ENTITY: &str = "IFCMATERIALLAYER";
178 let release = Release::of(model);
179 let values = layer_values(release, ENTITY, &draft)?;
180 if let Some(material) = draft.material {
181 require_type(tx, model, release, material, &["IFCMATERIAL"])?;
182 }
183 Ok(tx.create(release.record(ENTITY, values)?))
184}
185
186pub fn create_layer_set(
194 tx: &mut Transaction,
195 model: &Model,
196 draft: LayerSetDraft<'_>,
197) -> MaterialResult<EntityId> {
198 const ENTITY: &str = "IFCMATERIALLAYERSET";
199 let release = Release::of(model);
200 release.require_entity(ENTITY, None)?;
201 if draft.layers.is_empty() {
202 return Err(invalid(
203 ENTITY,
204 "MaterialLayers",
205 "expected at least one layer",
206 ));
207 }
208 for &layer in draft.layers {
209 require_type(
210 tx,
211 model,
212 release,
213 layer,
214 &["IFCMATERIALLAYER", "IFCMATERIALLAYERWITHOFFSETS"],
215 )?;
216 }
217 let record = release.record(
218 ENTITY,
219 vec![
220 ("MaterialLayers", refs(draft.layers)),
221 ("LayerSetName", optional_text(draft.name)),
222 ("Description", optional_text(draft.description)),
223 ],
224 )?;
225 Ok(tx.create(record))
226}
227
228pub fn associate_material(
241 tx: &mut Transaction,
242 model: &Model,
243 draft: MaterialAssignmentDraft<'_>,
244) -> MaterialResult<EntityId> {
245 associate(tx, model, draft, None)
246}
247
248pub fn associate_material_with_owner_history(
257 tx: &mut Transaction,
258 model: &Model,
259 draft: MaterialAssignmentDraft<'_>,
260 owner_history: EntityId,
261) -> MaterialResult<EntityId> {
262 associate(tx, model, draft, Some(owner_history))
263}
264
265fn associate(
266 tx: &mut Transaction,
267 model: &Model,
268 draft: MaterialAssignmentDraft<'_>,
269 owner_history: Option<EntityId>,
270) -> MaterialResult<EntityId> {
271 const ENTITY: &str = "IFCRELASSOCIATESMATERIAL";
272 let release = Release::of(model);
273 release.require_entity(ENTITY, None)?;
274 if ifc_model::guid::Guid::parse(draft.global_id).is_none() {
275 return Err(invalid(ENTITY, "GlobalId", "expected IFC compressed GUID"));
276 }
277 if draft.related_objects.is_empty() {
278 return Err(invalid(
279 ENTITY,
280 "RelatedObjects",
281 "expected at least one object",
282 ));
283 }
284 let mut unique = HashSet::new();
285 for &object in draft.related_objects {
286 if !unique.insert(object) {
287 return Err(invalid(
288 ENTITY,
289 "RelatedObjects",
290 "duplicate object reference",
291 ));
292 }
293 require_exists(tx, model, object)?;
294 }
295 require_accepts(
296 tx,
297 model,
298 release,
299 ENTITY,
300 "RelatingMaterial",
301 draft.relating_material,
302 )?;
303 if let Some(owner_history) = owner_history {
304 require_type(tx, model, release, owner_history, &["IFCOWNERHISTORY"])?;
305 }
306 let record = release.record(
307 ENTITY,
308 vec![
309 ("GlobalId", text(draft.global_id)),
310 (
311 "OwnerHistory",
312 owner_history.map_or(Value::Null, Value::Ref),
313 ),
314 ("Name", optional_text(draft.name)),
315 ("Description", optional_text(draft.description)),
316 ("RelatedObjects", refs(draft.related_objects)),
317 ("RelatingMaterial", Value::Ref(draft.relating_material)),
318 ],
319 )?;
320 Ok(tx.create(record))
321}
322
323pub fn create_material_list(
325 tx: &mut Transaction,
326 model: &Model,
327 materials: &[EntityId],
328) -> MaterialResult<EntityId> {
329 const ENTITY: &str = "IFCMATERIALLIST";
330 let release = Release::of(model);
331 release.require_entity(ENTITY, None)?;
332 if materials.is_empty() {
333 return Err(invalid(
334 ENTITY,
335 "Materials",
336 "expected at least one material",
337 ));
338 }
339 for &material in materials {
340 require_type(tx, model, release, material, &["IFCMATERIAL"])?;
341 }
342 Ok(tx.create(release.record(ENTITY, vec![("Materials", refs(materials))])?))
343}
344
345pub fn create_layer_set_usage(
355 tx: &mut Transaction,
356 model: &Model,
357 for_layer_set: EntityId,
358 direction: LayerSetDirection,
359 sense: DirectionSense,
360 offset_from_reference_line: f64,
361 reference_extent: Option<f64>,
362) -> MaterialResult<EntityId> {
363 const ENTITY: &str = "IFCMATERIALLAYERSETUSAGE";
364 let release = Release::of(model);
365 release.require_entity(ENTITY, None)?;
366 require_type(tx, model, release, for_layer_set, &["IFCMATERIALLAYERSET"])?;
367 if !offset_from_reference_line.is_finite() {
368 return Err(invalid(
369 ENTITY,
370 "OffsetFromReferenceLine",
371 "expected a finite length",
372 ));
373 }
374 let record = release.record(
375 ENTITY,
376 vec![
377 ("ForLayerSet", Value::Ref(for_layer_set)),
378 (
379 "LayerSetDirection",
380 Value::Enum(direction.as_token().into()),
381 ),
382 ("DirectionSense", Value::Enum(sense.as_token().into())),
383 (
384 "OffsetFromReferenceLine",
385 Value::Real(offset_from_reference_line),
386 ),
387 (
388 "ReferenceExtent",
389 reference_extent.map_or(Value::Null, Value::Real),
390 ),
391 ],
392 )?;
393 Ok(tx.create(record))
394}
395
396pub fn create_layer_with_offsets(
409 tx: &mut Transaction,
410 model: &Model,
411 draft: LayerDraft<'_>,
412 offset_direction: LayerSetDirection,
413 offset_values: [f64; 2],
414) -> MaterialResult<EntityId> {
415 const ENTITY: &str = "IFCMATERIALLAYERWITHOFFSETS";
416 let release = Release::of(model);
417 let mut values = layer_values(release, ENTITY, &draft)?;
418 if offset_values.iter().any(|value| !value.is_finite()) {
419 return Err(invalid(ENTITY, "OffsetValues", "expected finite lengths"));
420 }
421 if let Some(material) = draft.material {
422 require_type(tx, model, release, material, &["IFCMATERIAL"])?;
423 }
424 values.push((
425 "OffsetDirection",
426 Value::Enum(offset_direction.as_token().into()),
427 ));
428 values.push(("OffsetValues", reals(&offset_values)));
429 Ok(tx.create(release.record(ENTITY, values)?))
430}
431
432pub(crate) fn require_exists(tx: &Transaction, model: &Model, id: EntityId) -> MaterialResult<()> {
434 if type_name(tx, model, id).is_some() {
435 Ok(())
436 } else {
437 Err(MaterialError::UnknownEntity { id })
438 }
439}
440
441pub(crate) fn require_type(
444 tx: &Transaction,
445 model: &Model,
446 release: Release<'_>,
447 id: EntityId,
448 expected: &[&'static str],
449) -> MaterialResult<()> {
450 let actual = type_name(tx, model, id).ok_or(MaterialError::UnknownEntity { id })?;
451 if expected
452 .iter()
453 .any(|kind| actual.eq_ignore_ascii_case(kind) && release.instantiates(kind))
454 {
455 Ok(())
456 } else {
457 Err(MaterialError::AuthoringReferenceType {
458 target: id,
459 expected: expected[0],
460 actual: actual.to_owned(),
461 })
462 }
463}
464
465pub(crate) fn require_accepts(
469 tx: &Transaction,
470 model: &Model,
471 release: Release<'_>,
472 entity: &'static str,
473 attribute: &'static str,
474 id: EntityId,
475) -> MaterialResult<()> {
476 let actual = type_name(tx, model, id).ok_or(MaterialError::UnknownEntity { id })?;
477 let (accepted, declared) = release.accepts(entity, attribute, actual)?;
478 if accepted {
479 Ok(())
480 } else {
481 Err(MaterialError::AuthoringReferenceType {
482 target: id,
483 expected: declared,
484 actual: actual.to_owned(),
485 })
486 }
487}
488
489fn type_name<'a>(tx: &'a Transaction, model: &'a Model, id: EntityId) -> Option<&'a str> {
490 for edit in tx.edits().iter().rev() {
491 match edit {
492 Edit::Create {
493 id: candidate,
494 entity,
495 } if *candidate == id => return Some(&entity.type_name),
496 Edit::Retype {
497 id: candidate,
498 type_name,
499 } if *candidate == id => return Some(type_name),
500 Edit::Remove { id: candidate } if *candidate == id => return None,
501 _ => {}
502 }
503 }
504 model.get(id).map(|entity| entity.type_name.as_ref())
505}
506
507pub(crate) fn invalid(
508 entity: &'static str,
509 attribute: &'static str,
510 value: impl Into<String>,
511) -> MaterialError {
512 MaterialError::AuthoringInvalid {
513 entity,
514 attribute,
515 value: value.into(),
516 }
517}
518
519fn text(value: &str) -> Value {
520 Value::Text(value.into())
521}
522
523pub(crate) fn optional_text(value: Option<&str>) -> Value {
524 value.map_or(Value::Null, text)
525}
526
527pub(crate) fn refs(ids: &[EntityId]) -> Value {
528 Value::List(ids.iter().copied().map(Value::Ref).collect())
529}
530
531pub(crate) fn reals(values: &[f64]) -> Value {
532 Value::List(values.iter().copied().map(Value::Real).collect())
533}
534
535fn logical(value: LogicalValue) -> Value {
536 match value {
537 LogicalValue::False => Value::Bool(false),
538 LogicalValue::True => Value::Bool(true),
539 LogicalValue::Unknown => Value::LogicalUnknown,
540 }
541}