1mod relationships;
7
8pub use relationships::{
9 create_material_classification_relationship, create_material_definition_representation,
10 create_material_properties, create_material_relationship,
11};
12
13use std::collections::HashSet;
14
15use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
16
17use crate::{DirectionSense, LayerSetDirection, LogicalValue, MaterialError, MaterialResult};
18
19#[derive(Debug, Clone, Copy)]
21pub struct MaterialDraft<'a> {
22 pub name: &'a str,
24 pub description: Option<&'a str>,
26 pub category: Option<&'a str>,
28}
29
30#[derive(Debug, Clone, Copy)]
32pub struct LayerDraft<'a> {
33 pub material: Option<EntityId>,
35 pub thickness: f64,
37 pub is_ventilated: Option<LogicalValue>,
39 pub name: Option<&'a str>,
41 pub description: Option<&'a str>,
43 pub category: Option<&'a str>,
45 pub priority: Option<i64>,
47}
48
49#[derive(Debug, Clone, Copy)]
51pub struct LayerSetDraft<'a> {
52 pub layers: &'a [EntityId],
54 pub name: Option<&'a str>,
56 pub description: Option<&'a str>,
58}
59
60#[derive(Debug, Clone, Copy)]
62pub struct MaterialAssignmentDraft<'a> {
63 pub global_id: &'a str,
66 pub name: Option<&'a str>,
68 pub description: Option<&'a str>,
70 pub related_objects: &'a [EntityId],
73 pub relating_material: EntityId,
76}
77
78pub fn create_material(tx: &mut Transaction, draft: MaterialDraft<'_>) -> EntityId {
80 tx.create(Entity::new(
81 "IFCMATERIAL",
82 vec![
83 text(draft.name),
84 optional_text(draft.description),
85 optional_text(draft.category),
86 ],
87 ))
88}
89
90pub fn create_layer(
92 tx: &mut Transaction,
93 model: &Model,
94 draft: LayerDraft<'_>,
95) -> MaterialResult<EntityId> {
96 finite_non_negative("IFCMATERIALLAYER", "LayerThickness", draft.thickness)?;
97 if let Some(priority) = draft.priority.filter(|value| !(0..=100).contains(value)) {
98 return Err(invalid(
99 "IFCMATERIALLAYER",
100 "Priority",
101 priority.to_string(),
102 ));
103 }
104 if let Some(material) = draft.material {
105 require_type(tx, model, material, &["IFCMATERIAL"])?;
106 }
107 Ok(tx.create(Entity::new(
108 "IFCMATERIALLAYER",
109 vec![
110 draft.material.map_or(Value::Null, Value::Ref),
111 Value::Real(draft.thickness),
112 draft.is_ventilated.map_or(Value::Null, logical),
113 optional_text(draft.name),
114 optional_text(draft.description),
115 optional_text(draft.category),
116 draft.priority.map_or(Value::Null, Value::Integer),
117 ],
118 )))
119}
120
121pub fn create_layer_set(
124 tx: &mut Transaction,
125 model: &Model,
126 draft: LayerSetDraft<'_>,
127) -> MaterialResult<EntityId> {
128 if draft.layers.is_empty() {
129 return Err(invalid(
130 "IFCMATERIALLAYERSET",
131 "MaterialLayers",
132 "expected at least one layer",
133 ));
134 }
135 for &layer in draft.layers {
136 require_type(
137 tx,
138 model,
139 layer,
140 &["IFCMATERIALLAYER", "IFCMATERIALLAYERWITHOFFSETS"],
141 )?;
142 }
143 Ok(tx.create(Entity::new(
144 "IFCMATERIALLAYERSET",
145 vec![
146 refs(draft.layers),
147 optional_text(draft.name),
148 optional_text(draft.description),
149 ],
150 )))
151}
152
153pub fn associate_material(
156 tx: &mut Transaction,
157 model: &Model,
158 draft: MaterialAssignmentDraft<'_>,
159) -> MaterialResult<EntityId> {
160 if ifc_model::guid::Guid::parse(draft.global_id).is_none() {
161 return Err(invalid(
162 "IFCRELASSOCIATESMATERIAL",
163 "GlobalId",
164 "expected IFC compressed GUID",
165 ));
166 }
167 if draft.related_objects.is_empty() {
168 return Err(invalid(
169 "IFCRELASSOCIATESMATERIAL",
170 "RelatedObjects",
171 "expected at least one object",
172 ));
173 }
174 let mut unique = HashSet::new();
175 for &object in draft.related_objects {
176 if !unique.insert(object) {
177 return Err(invalid(
178 "IFCRELASSOCIATESMATERIAL",
179 "RelatedObjects",
180 "duplicate object reference",
181 ));
182 }
183 require_exists(tx, model, object)?;
184 }
185 require_type(tx, model, draft.relating_material, MATERIAL_SELECT_TYPES)?;
186 Ok(tx.create(Entity::new(
187 "IFCRELASSOCIATESMATERIAL",
188 vec![
189 text(draft.global_id),
190 Value::Null,
191 optional_text(draft.name),
192 optional_text(draft.description),
193 refs(draft.related_objects),
194 Value::Ref(draft.relating_material),
195 ],
196 )))
197}
198
199#[derive(Debug, Clone, Copy)]
201pub struct ConstituentDraft<'a> {
202 pub name: Option<&'a str>,
204 pub description: Option<&'a str>,
206 pub material: EntityId,
208 pub fraction: Option<f64>,
210 pub category: Option<&'a str>,
212}
213
214pub fn create_constituent(
220 tx: &mut Transaction,
221 model: &Model,
222 draft: ConstituentDraft<'_>,
223) -> MaterialResult<EntityId> {
224 if let Some(fraction) = draft.fraction {
225 if !fraction.is_finite() || !(0.0..=1.0).contains(&fraction) {
226 return Err(invalid(
227 "IFCMATERIALCONSTITUENT",
228 "Fraction",
229 "expected a normalised ratio in 0..=1",
230 ));
231 }
232 }
233 require_type(tx, model, draft.material, &["IFCMATERIAL"])?;
234 Ok(tx.create(Entity::new(
235 "IFCMATERIALCONSTITUENT",
236 vec![
237 optional_text(draft.name),
238 optional_text(draft.description),
239 Value::Ref(draft.material),
240 draft.fraction.map_or(Value::Null, Value::Real),
241 optional_text(draft.category),
242 ],
243 )))
244}
245
246pub fn create_constituent_set(
249 tx: &mut Transaction,
250 model: &Model,
251 constituents: &[EntityId],
252 name: Option<&str>,
253 description: Option<&str>,
254) -> MaterialResult<EntityId> {
255 if constituents.is_empty() {
256 return Err(invalid(
257 "IFCMATERIALCONSTITUENTSET",
258 "MaterialConstituents",
259 "expected at least one constituent",
260 ));
261 }
262 for &constituent in constituents {
263 require_type(tx, model, constituent, &["IFCMATERIALCONSTITUENT"])?;
264 }
265 Ok(tx.create(Entity::new(
266 "IFCMATERIALCONSTITUENTSET",
267 vec![
268 optional_text(name),
269 optional_text(description),
270 refs(constituents),
271 ],
272 )))
273}
274
275#[derive(Debug, Clone, Copy)]
277pub struct ProfileDraft<'a> {
278 pub name: Option<&'a str>,
280 pub description: Option<&'a str>,
282 pub material: Option<EntityId>,
284 pub profile: EntityId,
286 pub priority: Option<i64>,
288 pub category: Option<&'a str>,
290}
291
292pub fn create_profile(
298 tx: &mut Transaction,
299 model: &Model,
300 draft: ProfileDraft<'_>,
301) -> MaterialResult<EntityId> {
302 if let Some(priority) = draft.priority.filter(|value| !(0..=100).contains(value)) {
303 return Err(invalid(
304 "IFCMATERIALPROFILE",
305 "Priority",
306 priority.to_string(),
307 ));
308 }
309 if let Some(material) = draft.material {
310 require_type(tx, model, material, &["IFCMATERIAL"])?;
311 }
312 require_exists(tx, model, draft.profile)?;
313 Ok(tx.create(Entity::new(
314 "IFCMATERIALPROFILE",
315 vec![
316 optional_text(draft.name),
317 optional_text(draft.description),
318 draft.material.map_or(Value::Null, Value::Ref),
319 Value::Ref(draft.profile),
320 draft.priority.map_or(Value::Null, Value::Integer),
321 optional_text(draft.category),
322 ],
323 )))
324}
325
326pub fn create_profile_set(
328 tx: &mut Transaction,
329 model: &Model,
330 profiles: &[EntityId],
331 name: Option<&str>,
332 description: Option<&str>,
333 composite_profile: Option<EntityId>,
334) -> MaterialResult<EntityId> {
335 if profiles.is_empty() {
336 return Err(invalid(
337 "IFCMATERIALPROFILESET",
338 "MaterialProfiles",
339 "expected at least one profile",
340 ));
341 }
342 for &profile in profiles {
343 require_type(tx, model, profile, &["IFCMATERIALPROFILE"])?;
344 }
345 if let Some(composite) = composite_profile {
346 require_exists(tx, model, composite)?;
347 }
348 Ok(tx.create(Entity::new(
349 "IFCMATERIALPROFILESET",
350 vec![
351 optional_text(name),
352 optional_text(description),
353 refs(profiles),
354 composite_profile.map_or(Value::Null, Value::Ref),
355 ],
356 )))
357}
358
359pub fn create_material_list(
361 tx: &mut Transaction,
362 model: &Model,
363 materials: &[EntityId],
364) -> MaterialResult<EntityId> {
365 if materials.is_empty() {
366 return Err(invalid(
367 "IFCMATERIALLIST",
368 "Materials",
369 "expected at least one material",
370 ));
371 }
372 for &material in materials {
373 require_type(tx, model, material, &["IFCMATERIAL"])?;
374 }
375 Ok(tx.create(Entity::new("IFCMATERIALLIST", vec![refs(materials)])))
376}
377
378pub fn create_layer_set_usage(
383 tx: &mut Transaction,
384 model: &Model,
385 for_layer_set: EntityId,
386 direction: LayerSetDirection,
387 sense: DirectionSense,
388 offset_from_reference_line: f64,
389 reference_extent: Option<f64>,
390) -> MaterialResult<EntityId> {
391 require_type(tx, model, for_layer_set, &["IFCMATERIALLAYERSET"])?;
392 if !offset_from_reference_line.is_finite() {
393 return Err(invalid(
394 "IFCMATERIALLAYERSETUSAGE",
395 "OffsetFromReferenceLine",
396 "expected a finite length",
397 ));
398 }
399 Ok(tx.create(Entity::new(
400 "IFCMATERIALLAYERSETUSAGE",
401 vec![
402 Value::Ref(for_layer_set),
403 Value::Enum(direction.as_token().into()),
404 Value::Enum(sense.as_token().into()),
405 Value::Real(offset_from_reference_line),
406 reference_extent.map_or(Value::Null, Value::Real),
407 ],
408 )))
409}
410
411pub fn create_profile_set_usage(
416 tx: &mut Transaction,
417 model: &Model,
418 for_profile_set: EntityId,
419 cardinal_point: Option<i64>,
420 reference_extent: Option<f64>,
421) -> MaterialResult<EntityId> {
422 require_type(tx, model, for_profile_set, &["IFCMATERIALPROFILESET"])?;
423 if let Some(point) = cardinal_point.filter(|value| !(1..=9).contains(value)) {
424 return Err(invalid(
425 "IFCMATERIALPROFILESETUSAGE",
426 "CardinalPoint",
427 point.to_string(),
428 ));
429 }
430 Ok(tx.create(Entity::new(
431 "IFCMATERIALPROFILESETUSAGE",
432 vec![
433 Value::Ref(for_profile_set),
434 cardinal_point.map_or(Value::Null, Value::Integer),
435 reference_extent.map_or(Value::Null, Value::Real),
436 ],
437 )))
438}
439
440pub fn create_layer_with_offsets(
448 tx: &mut Transaction,
449 model: &Model,
450 draft: LayerDraft<'_>,
451 offset_direction: LayerSetDirection,
452 offset_values: [f64; 2],
453) -> MaterialResult<EntityId> {
454 finite_non_negative(
455 "IFCMATERIALLAYERWITHOFFSETS",
456 "LayerThickness",
457 draft.thickness,
458 )?;
459 for value in offset_values {
460 if !value.is_finite() {
461 return Err(invalid(
462 "IFCMATERIALLAYERWITHOFFSETS",
463 "OffsetValues",
464 "expected finite lengths",
465 ));
466 }
467 }
468 if let Some(material) = draft.material {
469 require_type(tx, model, material, &["IFCMATERIAL"])?;
470 }
471 Ok(tx.create(Entity::new(
472 "IFCMATERIALLAYERWITHOFFSETS",
473 vec![
474 draft.material.map_or(Value::Null, Value::Ref),
475 Value::Real(draft.thickness),
476 draft.is_ventilated.map_or(Value::Null, logical),
477 optional_text(draft.name),
478 optional_text(draft.description),
479 optional_text(draft.category),
480 draft.priority.map_or(Value::Null, Value::Integer),
481 Value::Enum(offset_direction.as_token().into()),
482 Value::List(offset_values.iter().copied().map(Value::Real).collect()),
483 ],
484 )))
485}
486
487pub fn create_profile_with_offsets(
498 tx: &mut Transaction,
499 model: &Model,
500 draft: ProfileDraft<'_>,
501 offset_values: [f64; 2],
502) -> MaterialResult<EntityId> {
503 if let Some(priority) = draft.priority.filter(|value| !(0..=100).contains(value)) {
504 return Err(invalid(
505 "IFCMATERIALPROFILEWITHOFFSETS",
506 "Priority",
507 priority.to_string(),
508 ));
509 }
510 for value in offset_values {
511 if !value.is_finite() {
512 return Err(invalid(
513 "IFCMATERIALPROFILEWITHOFFSETS",
514 "OffsetValues",
515 "expected finite lengths",
516 ));
517 }
518 }
519 if let Some(material) = draft.material {
520 require_type(tx, model, material, &["IFCMATERIAL"])?;
521 }
522 require_exists(tx, model, draft.profile)?;
523 Ok(tx.create(Entity::new(
524 "IFCMATERIALPROFILEWITHOFFSETS",
525 vec![
526 optional_text(draft.name),
527 optional_text(draft.description),
528 draft.material.map_or(Value::Null, Value::Ref),
529 Value::Ref(draft.profile),
530 draft.priority.map_or(Value::Null, Value::Integer),
531 optional_text(draft.category),
532 Value::List(offset_values.iter().copied().map(Value::Real).collect()),
533 ],
534 )))
535}
536
537pub fn create_profile_set_usage_tapering(
546 tx: &mut Transaction,
547 model: &Model,
548 for_profile_set: EntityId,
549 for_profile_end_set: EntityId,
550 cardinal_point: Option<i64>,
551 cardinal_end_point: Option<i64>,
552 reference_extent: Option<f64>,
553) -> MaterialResult<EntityId> {
554 const ENTITY: &str = "IFCMATERIALPROFILESETUSAGETAPERING";
555 for set in [for_profile_set, for_profile_end_set] {
556 require_type(tx, model, set, &["IFCMATERIALPROFILESET"])?;
557 }
558 for (point, attribute) in [
560 (cardinal_point, "CardinalPoint"),
561 (cardinal_end_point, "CardinalEndPoint"),
562 ] {
563 if let Some(value) = point.filter(|value| !(1..=9).contains(value)) {
564 return Err(invalid(ENTITY, attribute, value.to_string()));
565 }
566 }
567 Ok(tx.create(Entity::new(
568 ENTITY,
569 vec![
570 Value::Ref(for_profile_set),
571 cardinal_point.map_or(Value::Null, Value::Integer),
572 reference_extent.map_or(Value::Null, Value::Real),
573 Value::Ref(for_profile_end_set),
574 cardinal_end_point.map_or(Value::Null, Value::Integer),
575 ],
576 )))
577}
578
579const MATERIAL_SELECT_TYPES: &[&str] = &[
580 "IFCMATERIAL",
581 "IFCMATERIALLIST",
582 "IFCMATERIALLAYERSET",
583 "IFCMATERIALPROFILESET",
584 "IFCMATERIALCONSTITUENTSET",
585 "IFCMATERIALLAYERSETUSAGE",
586 "IFCMATERIALPROFILESETUSAGE",
587 "IFCMATERIALPROFILESETUSAGETAPERING",
588];
589
590fn require_exists(tx: &Transaction, model: &Model, id: EntityId) -> MaterialResult<()> {
591 if type_name(tx, model, id).is_some() {
592 Ok(())
593 } else {
594 Err(MaterialError::UnknownEntity { id })
595 }
596}
597fn require_type(
598 tx: &Transaction,
599 model: &Model,
600 id: EntityId,
601 expected: &[&'static str],
602) -> MaterialResult<()> {
603 let actual = type_name(tx, model, id).ok_or(MaterialError::UnknownEntity { id })?;
604 if expected
605 .iter()
606 .any(|kind| actual.eq_ignore_ascii_case(kind))
607 {
608 Ok(())
609 } else {
610 Err(MaterialError::AuthoringReferenceType {
611 target: id,
612 expected: expected[0],
613 actual: actual.to_owned(),
614 })
615 }
616}
617fn type_name<'a>(tx: &'a Transaction, model: &'a Model, id: EntityId) -> Option<&'a str> {
618 for edit in tx.edits().iter().rev() {
619 match edit {
620 Edit::Create {
621 id: candidate,
622 entity,
623 } if *candidate == id => return Some(&entity.type_name),
624 Edit::Retype {
625 id: candidate,
626 type_name,
627 } if *candidate == id => return Some(type_name),
628 Edit::Remove { id: candidate } if *candidate == id => return None,
629 _ => {}
630 }
631 }
632 model.get(id).map(|entity| entity.type_name.as_ref())
633}
634fn finite_non_negative(
635 entity: &'static str,
636 attribute: &'static str,
637 value: f64,
638) -> MaterialResult<()> {
639 if value.is_finite() && value >= 0.0 {
640 Ok(())
641 } else {
642 Err(invalid(
643 entity,
644 attribute,
645 "expected a finite non-negative length",
646 ))
647 }
648}
649fn invalid(
650 entity: &'static str,
651 attribute: &'static str,
652 value: impl Into<String>,
653) -> MaterialError {
654 MaterialError::AuthoringInvalid {
655 entity,
656 attribute,
657 value: value.into(),
658 }
659}
660fn text(value: &str) -> Value {
661 Value::Text(value.into())
662}
663fn optional_text(value: Option<&str>) -> Value {
664 value.map_or(Value::Null, text)
665}
666fn refs(ids: &[EntityId]) -> Value {
667 Value::List(ids.iter().copied().map(Value::Ref).collect())
668}
669fn logical(value: LogicalValue) -> Value {
670 match value {
671 LogicalValue::False => Value::Bool(false),
672 LogicalValue::True => Value::Bool(true),
673 LogicalValue::Unknown => Value::LogicalUnknown,
674 }
675}