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)]
37#[non_exhaustive]
38pub struct MaterialDraft<'a> {
39 pub name: &'a str,
41 pub description: Option<&'a str>,
43 pub category: Option<&'a str>,
45}
46
47impl<'a> MaterialDraft<'a> {
48 #[must_use]
50 pub const fn new(name: &'a str) -> Self {
51 Self {
52 name,
53 description: None,
54 category: None,
55 }
56 }
57
58 #[must_use]
60 pub const fn description(mut self, value: &'a str) -> Self {
61 self.description = Some(value);
62 self
63 }
64
65 #[must_use]
67 pub const fn category(mut self, value: &'a str) -> Self {
68 self.category = Some(value);
69 self
70 }
71}
72
73#[derive(Debug, Clone, Copy)]
75#[non_exhaustive]
76pub struct LayerDraft<'a> {
77 pub material: Option<EntityId>,
79 pub thickness: f64,
82 pub is_ventilated: Option<LogicalValue>,
84 pub name: Option<&'a str>,
86 pub description: Option<&'a str>,
88 pub category: Option<&'a str>,
90 pub priority: Option<i64>,
93}
94
95impl<'a> LayerDraft<'a> {
96 #[must_use]
98 pub const fn new(thickness: f64) -> Self {
99 Self {
100 material: None,
101 thickness,
102 is_ventilated: None,
103 name: None,
104 description: None,
105 category: None,
106 priority: None,
107 }
108 }
109
110 #[must_use]
112 pub const fn material(mut self, value: EntityId) -> Self {
113 self.material = Some(value);
114 self
115 }
116
117 #[must_use]
119 pub const fn is_ventilated(mut self, value: LogicalValue) -> Self {
120 self.is_ventilated = Some(value);
121 self
122 }
123
124 #[must_use]
126 pub const fn name(mut self, value: &'a str) -> Self {
127 self.name = Some(value);
128 self
129 }
130
131 #[must_use]
133 pub const fn description(mut self, value: &'a str) -> Self {
134 self.description = Some(value);
135 self
136 }
137
138 #[must_use]
140 pub const fn category(mut self, value: &'a str) -> Self {
141 self.category = Some(value);
142 self
143 }
144
145 #[must_use]
147 pub const fn priority(mut self, value: i64) -> Self {
148 self.priority = Some(value);
149 self
150 }
151}
152
153#[derive(Debug, Clone, Copy)]
155#[non_exhaustive]
156pub struct LayerSetDraft<'a> {
157 pub layers: &'a [EntityId],
159 pub name: Option<&'a str>,
161 pub description: Option<&'a str>,
163}
164
165impl<'a> LayerSetDraft<'a> {
166 #[must_use]
168 pub const fn new(layers: &'a [EntityId]) -> Self {
169 Self {
170 layers,
171 name: None,
172 description: None,
173 }
174 }
175
176 #[must_use]
178 pub const fn name(mut self, value: &'a str) -> Self {
179 self.name = Some(value);
180 self
181 }
182
183 #[must_use]
185 pub const fn description(mut self, value: &'a str) -> Self {
186 self.description = Some(value);
187 self
188 }
189}
190
191#[derive(Debug, Clone, Copy)]
193#[non_exhaustive]
194pub struct MaterialAssignmentDraft<'a> {
195 pub global_id: &'a str,
198 pub name: Option<&'a str>,
200 pub description: Option<&'a str>,
202 pub related_objects: &'a [EntityId],
205 pub relating_material: EntityId,
208}
209
210impl<'a> MaterialAssignmentDraft<'a> {
211 #[must_use]
214 pub const fn new(
215 global_id: &'a str,
216 related_objects: &'a [EntityId],
217 relating_material: EntityId,
218 ) -> Self {
219 Self {
220 global_id,
221 name: None,
222 description: None,
223 related_objects,
224 relating_material,
225 }
226 }
227
228 #[must_use]
230 pub const fn name(mut self, value: &'a str) -> Self {
231 self.name = Some(value);
232 self
233 }
234
235 #[must_use]
237 pub const fn description(mut self, value: &'a str) -> Self {
238 self.description = Some(value);
239 self
240 }
241}
242
243pub fn create_material(
251 tx: &mut Transaction,
252 model: &Model,
253 draft: MaterialDraft<'_>,
254) -> MaterialResult<EntityId> {
255 let record = Release::of(model).record(
256 "IFCMATERIAL",
257 vec![
258 ("Name", text(draft.name)),
259 ("Description", optional_text(draft.description)),
260 ("Category", optional_text(draft.category)),
261 ],
262 )?;
263 Ok(tx.create(record))
264}
265
266fn layer_values(
269 release: Release<'_>,
270 entity: &'static str,
271 draft: &LayerDraft<'_>,
272) -> MaterialResult<Vec<(&'static str, Value)>> {
273 let declared = release.declared(entity, "LayerThickness")?;
274 let positive = declared
275 .type_name
276 .eq_ignore_ascii_case("IfcPositiveLengthMeasure");
277 if !draft.thickness.is_finite() || draft.thickness < 0.0 {
278 return Err(invalid(
279 entity,
280 "LayerThickness",
281 "expected a finite non-negative length",
282 ));
283 }
284 if positive && draft.thickness == 0.0 {
285 return Err(invalid(
286 entity,
287 "LayerThickness",
288 "expected a positive length (IfcPositiveLengthMeasure)",
289 ));
290 }
291 if let Some(priority) = draft.priority.filter(|value| !(0..=100).contains(value)) {
292 return Err(invalid(entity, "Priority", priority.to_string()));
293 }
294 Ok(vec![
295 ("Material", draft.material.map_or(Value::Null, Value::Ref)),
296 ("LayerThickness", Value::Real(draft.thickness)),
297 (
298 "IsVentilated",
299 draft.is_ventilated.map_or(Value::Null, logical),
300 ),
301 ("Name", optional_text(draft.name)),
302 ("Description", optional_text(draft.description)),
303 ("Category", optional_text(draft.category)),
304 (
305 "Priority",
306 draft.priority.map_or(Value::Null, Value::Integer),
307 ),
308 ])
309}
310
311pub fn create_layer(
320 tx: &mut Transaction,
321 model: &Model,
322 draft: LayerDraft<'_>,
323) -> MaterialResult<EntityId> {
324 const ENTITY: &str = "IFCMATERIALLAYER";
325 let release = Release::of(model);
326 let values = layer_values(release, ENTITY, &draft)?;
327 if let Some(material) = draft.material {
328 require_type(tx, model, release, material, &["IFCMATERIAL"])?;
329 }
330 Ok(tx.create(release.record(ENTITY, values)?))
331}
332
333pub fn create_layer_set(
341 tx: &mut Transaction,
342 model: &Model,
343 draft: LayerSetDraft<'_>,
344) -> MaterialResult<EntityId> {
345 const ENTITY: &str = "IFCMATERIALLAYERSET";
346 let release = Release::of(model);
347 release.require_entity(ENTITY, None)?;
348 if draft.layers.is_empty() {
349 return Err(invalid(
350 ENTITY,
351 "MaterialLayers",
352 "expected at least one layer",
353 ));
354 }
355 for &layer in draft.layers {
356 require_type(
357 tx,
358 model,
359 release,
360 layer,
361 &["IFCMATERIALLAYER", "IFCMATERIALLAYERWITHOFFSETS"],
362 )?;
363 }
364 let record = release.record(
365 ENTITY,
366 vec![
367 ("MaterialLayers", refs(draft.layers)),
368 ("LayerSetName", optional_text(draft.name)),
369 ("Description", optional_text(draft.description)),
370 ],
371 )?;
372 Ok(tx.create(record))
373}
374
375pub fn associate_material(
388 tx: &mut Transaction,
389 model: &Model,
390 draft: MaterialAssignmentDraft<'_>,
391) -> MaterialResult<EntityId> {
392 associate(tx, model, draft, None)
393}
394
395pub fn associate_material_with_owner_history(
404 tx: &mut Transaction,
405 model: &Model,
406 draft: MaterialAssignmentDraft<'_>,
407 owner_history: EntityId,
408) -> MaterialResult<EntityId> {
409 associate(tx, model, draft, Some(owner_history))
410}
411
412fn associate(
413 tx: &mut Transaction,
414 model: &Model,
415 draft: MaterialAssignmentDraft<'_>,
416 owner_history: Option<EntityId>,
417) -> MaterialResult<EntityId> {
418 const ENTITY: &str = "IFCRELASSOCIATESMATERIAL";
419 let release = Release::of(model);
420 release.require_entity(ENTITY, None)?;
421 if ifc_model::guid::Guid::parse(draft.global_id).is_none() {
422 return Err(invalid(ENTITY, "GlobalId", "expected IFC compressed GUID"));
423 }
424 if draft.related_objects.is_empty() {
425 return Err(invalid(
426 ENTITY,
427 "RelatedObjects",
428 "expected at least one object",
429 ));
430 }
431 let mut unique = HashSet::new();
432 for &object in draft.related_objects {
433 if !unique.insert(object) {
434 return Err(invalid(
435 ENTITY,
436 "RelatedObjects",
437 "duplicate object reference",
438 ));
439 }
440 require_exists(tx, model, object)?;
441 }
442 require_accepts(
443 tx,
444 model,
445 release,
446 ENTITY,
447 "RelatingMaterial",
448 draft.relating_material,
449 )?;
450 if let Some(owner_history) = owner_history {
451 require_type(tx, model, release, owner_history, &["IFCOWNERHISTORY"])?;
452 }
453 let record = release.record(
454 ENTITY,
455 vec![
456 ("GlobalId", text(draft.global_id)),
457 (
458 "OwnerHistory",
459 owner_history.map_or(Value::Null, Value::Ref),
460 ),
461 ("Name", optional_text(draft.name)),
462 ("Description", optional_text(draft.description)),
463 ("RelatedObjects", refs(draft.related_objects)),
464 ("RelatingMaterial", Value::Ref(draft.relating_material)),
465 ],
466 )?;
467 Ok(tx.create(record))
468}
469
470pub fn create_material_list(
472 tx: &mut Transaction,
473 model: &Model,
474 materials: &[EntityId],
475) -> MaterialResult<EntityId> {
476 const ENTITY: &str = "IFCMATERIALLIST";
477 let release = Release::of(model);
478 release.require_entity(ENTITY, None)?;
479 if materials.is_empty() {
480 return Err(invalid(
481 ENTITY,
482 "Materials",
483 "expected at least one material",
484 ));
485 }
486 for &material in materials {
487 require_type(tx, model, release, material, &["IFCMATERIAL"])?;
488 }
489 Ok(tx.create(release.record(ENTITY, vec![("Materials", refs(materials))])?))
490}
491
492pub fn create_layer_set_usage(
502 tx: &mut Transaction,
503 model: &Model,
504 for_layer_set: EntityId,
505 direction: LayerSetDirection,
506 sense: DirectionSense,
507 offset_from_reference_line: f64,
508 reference_extent: Option<f64>,
509) -> MaterialResult<EntityId> {
510 const ENTITY: &str = "IFCMATERIALLAYERSETUSAGE";
511 let release = Release::of(model);
512 release.require_entity(ENTITY, None)?;
513 require_type(tx, model, release, for_layer_set, &["IFCMATERIALLAYERSET"])?;
514 if !offset_from_reference_line.is_finite() {
515 return Err(invalid(
516 ENTITY,
517 "OffsetFromReferenceLine",
518 "expected a finite length",
519 ));
520 }
521 let record = release.record(
522 ENTITY,
523 vec![
524 ("ForLayerSet", Value::Ref(for_layer_set)),
525 (
526 "LayerSetDirection",
527 Value::Enum(direction.as_token().into()),
528 ),
529 ("DirectionSense", Value::Enum(sense.as_token().into())),
530 (
531 "OffsetFromReferenceLine",
532 Value::Real(offset_from_reference_line),
533 ),
534 (
535 "ReferenceExtent",
536 reference_extent.map_or(Value::Null, Value::Real),
537 ),
538 ],
539 )?;
540 Ok(tx.create(record))
541}
542
543pub fn create_layer_with_offsets(
556 tx: &mut Transaction,
557 model: &Model,
558 draft: LayerDraft<'_>,
559 offset_direction: LayerSetDirection,
560 offset_values: [f64; 2],
561) -> MaterialResult<EntityId> {
562 const ENTITY: &str = "IFCMATERIALLAYERWITHOFFSETS";
563 let release = Release::of(model);
564 let mut values = layer_values(release, ENTITY, &draft)?;
565 if offset_values.iter().any(|value| !value.is_finite()) {
566 return Err(invalid(ENTITY, "OffsetValues", "expected finite lengths"));
567 }
568 if let Some(material) = draft.material {
569 require_type(tx, model, release, material, &["IFCMATERIAL"])?;
570 }
571 values.push((
572 "OffsetDirection",
573 Value::Enum(offset_direction.as_token().into()),
574 ));
575 values.push(("OffsetValues", reals(&offset_values)));
576 Ok(tx.create(release.record(ENTITY, values)?))
577}
578
579pub(crate) fn require_exists(tx: &Transaction, model: &Model, id: EntityId) -> MaterialResult<()> {
581 if type_name(tx, model, id).is_some() {
582 Ok(())
583 } else {
584 Err(MaterialError::UnknownEntity { id })
585 }
586}
587
588pub(crate) fn require_type(
591 tx: &Transaction,
592 model: &Model,
593 release: Release<'_>,
594 id: EntityId,
595 expected: &[&'static str],
596) -> MaterialResult<()> {
597 let actual = type_name(tx, model, id).ok_or(MaterialError::UnknownEntity { id })?;
598 if expected
599 .iter()
600 .any(|kind| actual.eq_ignore_ascii_case(kind) && release.instantiates(kind))
601 {
602 Ok(())
603 } else {
604 Err(MaterialError::AuthoringReferenceType {
605 target: id,
606 expected: expected[0],
607 actual: actual.to_owned(),
608 })
609 }
610}
611
612pub(crate) fn require_accepts(
616 tx: &Transaction,
617 model: &Model,
618 release: Release<'_>,
619 entity: &'static str,
620 attribute: &'static str,
621 id: EntityId,
622) -> MaterialResult<()> {
623 let actual = type_name(tx, model, id).ok_or(MaterialError::UnknownEntity { id })?;
624 let (accepted, declared) = release.accepts(entity, attribute, actual)?;
625 if accepted {
626 Ok(())
627 } else {
628 Err(MaterialError::AuthoringReferenceType {
629 target: id,
630 expected: declared,
631 actual: actual.to_owned(),
632 })
633 }
634}
635
636fn type_name<'a>(tx: &'a Transaction, model: &'a Model, id: EntityId) -> Option<&'a str> {
637 for edit in tx.edits().iter().rev() {
638 match edit {
639 Edit::Create {
640 id: candidate,
641 entity,
642 } if *candidate == id => return Some(&entity.type_name),
643 Edit::Retype {
644 id: candidate,
645 type_name,
646 } if *candidate == id => return Some(type_name),
647 Edit::Remove { id: candidate } if *candidate == id => return None,
648 _ => {}
649 }
650 }
651 model.get(id).map(|entity| entity.type_name.as_ref())
652}
653
654pub(crate) fn invalid(
655 entity: &'static str,
656 attribute: &'static str,
657 value: impl Into<String>,
658) -> MaterialError {
659 MaterialError::AuthoringInvalid {
660 entity,
661 attribute,
662 value: value.into(),
663 }
664}
665
666fn text(value: &str) -> Value {
667 Value::Text(value.into())
668}
669
670pub(crate) fn optional_text(value: Option<&str>) -> Value {
671 value.map_or(Value::Null, text)
672}
673
674pub(crate) fn refs(ids: &[EntityId]) -> Value {
675 Value::List(ids.iter().copied().map(Value::Ref).collect())
676}
677
678pub(crate) fn reals(values: &[f64]) -> Value {
679 Value::List(values.iter().copied().map(Value::Real).collect())
680}
681
682fn logical(value: LogicalValue) -> Value {
683 match value {
684 LogicalValue::False => Value::Bool(false),
685 LogicalValue::True => Value::Bool(true),
686 LogicalValue::Unknown => Value::LogicalUnknown,
687 }
688}