1use ifc_model::{EntityId, Model, Transaction, Value};
10
11use super::{invalid, optional_text, reals, refs, require_exists, require_type};
12use crate::release::Release;
13use crate::MaterialResult;
14
15#[derive(Debug, Clone, Copy)]
17pub struct ConstituentDraft<'a> {
18 pub name: Option<&'a str>,
20 pub description: Option<&'a str>,
22 pub material: EntityId,
24 pub fraction: Option<f64>,
26 pub category: Option<&'a str>,
28}
29
30pub fn create_constituent(
36 tx: &mut Transaction,
37 model: &Model,
38 draft: ConstituentDraft<'_>,
39) -> MaterialResult<EntityId> {
40 const ENTITY: &str = "IFCMATERIALCONSTITUENT";
41 let release = Release::of(model);
42 release.require_entity(ENTITY, None)?;
43 if let Some(fraction) = draft.fraction {
44 if !fraction.is_finite() || !(0.0..=1.0).contains(&fraction) {
45 return Err(invalid(
46 ENTITY,
47 "Fraction",
48 "expected a normalised ratio in 0..=1",
49 ));
50 }
51 }
52 require_type(tx, model, release, draft.material, &["IFCMATERIAL"])?;
53 let record = release.record(
54 ENTITY,
55 vec![
56 ("Name", optional_text(draft.name)),
57 ("Description", optional_text(draft.description)),
58 ("Material", Value::Ref(draft.material)),
59 ("Fraction", draft.fraction.map_or(Value::Null, Value::Real)),
60 ("Category", optional_text(draft.category)),
61 ],
62 )?;
63 Ok(tx.create(record))
64}
65
66pub fn create_constituent_set(
69 tx: &mut Transaction,
70 model: &Model,
71 constituents: &[EntityId],
72 name: Option<&str>,
73 description: Option<&str>,
74) -> MaterialResult<EntityId> {
75 const ENTITY: &str = "IFCMATERIALCONSTITUENTSET";
76 let release = Release::of(model);
77 release.require_entity(ENTITY, None)?;
78 if constituents.is_empty() {
79 return Err(invalid(
80 ENTITY,
81 "MaterialConstituents",
82 "expected at least one constituent",
83 ));
84 }
85 for &constituent in constituents {
86 require_type(tx, model, release, constituent, &["IFCMATERIALCONSTITUENT"])?;
87 }
88 let record = release.record(
89 ENTITY,
90 vec![
91 ("Name", optional_text(name)),
92 ("Description", optional_text(description)),
93 ("MaterialConstituents", refs(constituents)),
94 ],
95 )?;
96 Ok(tx.create(record))
97}
98
99#[derive(Debug, Clone, Copy)]
101pub struct ProfileDraft<'a> {
102 pub name: Option<&'a str>,
104 pub description: Option<&'a str>,
106 pub material: Option<EntityId>,
108 pub profile: EntityId,
110 pub priority: Option<i64>,
112 pub category: Option<&'a str>,
114}
115
116fn profile_values(
119 tx: &Transaction,
120 model: &Model,
121 release: Release<'_>,
122 entity: &'static str,
123 draft: &ProfileDraft<'_>,
124) -> MaterialResult<Vec<(&'static str, Value)>> {
125 release.require_entity(entity, None)?;
126 if let Some(priority) = draft.priority.filter(|value| !(0..=100).contains(value)) {
127 return Err(invalid(entity, "Priority", priority.to_string()));
128 }
129 if let Some(material) = draft.material {
130 require_type(tx, model, release, material, &["IFCMATERIAL"])?;
131 }
132 require_exists(tx, model, draft.profile)?;
133 Ok(vec![
134 ("Name", optional_text(draft.name)),
135 ("Description", optional_text(draft.description)),
136 ("Material", draft.material.map_or(Value::Null, Value::Ref)),
137 ("Profile", Value::Ref(draft.profile)),
138 (
139 "Priority",
140 draft.priority.map_or(Value::Null, Value::Integer),
141 ),
142 ("Category", optional_text(draft.category)),
143 ])
144}
145
146pub fn create_profile(
151 tx: &mut Transaction,
152 model: &Model,
153 draft: ProfileDraft<'_>,
154) -> MaterialResult<EntityId> {
155 const ENTITY: &str = "IFCMATERIALPROFILE";
156 let release = Release::of(model);
157 let values = profile_values(tx, model, release, ENTITY, &draft)?;
158 Ok(tx.create(release.record(ENTITY, values)?))
159}
160
161pub fn create_profile_set(
164 tx: &mut Transaction,
165 model: &Model,
166 profiles: &[EntityId],
167 name: Option<&str>,
168 description: Option<&str>,
169 composite_profile: Option<EntityId>,
170) -> MaterialResult<EntityId> {
171 const ENTITY: &str = "IFCMATERIALPROFILESET";
172 let release = Release::of(model);
173 release.require_entity(ENTITY, None)?;
174 if profiles.is_empty() {
175 return Err(invalid(
176 ENTITY,
177 "MaterialProfiles",
178 "expected at least one profile",
179 ));
180 }
181 for &profile in profiles {
182 require_type(tx, model, release, profile, &["IFCMATERIALPROFILE"])?;
183 }
184 if let Some(composite) = composite_profile {
185 require_exists(tx, model, composite)?;
186 }
187 let record = release.record(
188 ENTITY,
189 vec![
190 ("Name", optional_text(name)),
191 ("Description", optional_text(description)),
192 ("MaterialProfiles", refs(profiles)),
193 (
194 "CompositeProfile",
195 composite_profile.map_or(Value::Null, Value::Ref),
196 ),
197 ],
198 )?;
199 Ok(tx.create(record))
200}
201
202pub fn create_profile_set_usage(
207 tx: &mut Transaction,
208 model: &Model,
209 for_profile_set: EntityId,
210 cardinal_point: Option<i64>,
211 reference_extent: Option<f64>,
212) -> MaterialResult<EntityId> {
213 const ENTITY: &str = "IFCMATERIALPROFILESETUSAGE";
214 let release = Release::of(model);
215 release.require_entity(ENTITY, None)?;
216 require_type(
217 tx,
218 model,
219 release,
220 for_profile_set,
221 &["IFCMATERIALPROFILESET"],
222 )?;
223 if let Some(point) = cardinal_point.filter(|value| !(1..=9).contains(value)) {
224 return Err(invalid(ENTITY, "CardinalPoint", point.to_string()));
225 }
226 let record = release.record(
227 ENTITY,
228 vec![
229 ("ForProfileSet", Value::Ref(for_profile_set)),
230 (
231 "CardinalPoint",
232 cardinal_point.map_or(Value::Null, Value::Integer),
233 ),
234 (
235 "ReferenceExtent",
236 reference_extent.map_or(Value::Null, Value::Real),
237 ),
238 ],
239 )?;
240 Ok(tx.create(record))
241}
242
243pub fn create_profile_with_offsets(
254 tx: &mut Transaction,
255 model: &Model,
256 draft: ProfileDraft<'_>,
257 offset_values: [f64; 2],
258) -> MaterialResult<EntityId> {
259 const ENTITY: &str = "IFCMATERIALPROFILEWITHOFFSETS";
260 let release = Release::of(model);
261 release.require_entity(ENTITY, None)?;
262 if offset_values.iter().any(|value| !value.is_finite()) {
263 return Err(invalid(ENTITY, "OffsetValues", "expected finite lengths"));
264 }
265 let mut values = profile_values(tx, model, release, ENTITY, &draft)?;
266 values.push(("OffsetValues", reals(&offset_values)));
267 Ok(tx.create(release.record(ENTITY, values)?))
268}
269
270pub fn create_profile_set_usage_tapering(
279 tx: &mut Transaction,
280 model: &Model,
281 for_profile_set: EntityId,
282 for_profile_end_set: EntityId,
283 cardinal_point: Option<i64>,
284 cardinal_end_point: Option<i64>,
285 reference_extent: Option<f64>,
286) -> MaterialResult<EntityId> {
287 const ENTITY: &str = "IFCMATERIALPROFILESETUSAGETAPERING";
288 let release = Release::of(model);
289 release.require_entity(ENTITY, None)?;
290 for set in [for_profile_set, for_profile_end_set] {
291 require_type(tx, model, release, set, &["IFCMATERIALPROFILESET"])?;
292 }
293 for (point, attribute) in [
295 (cardinal_point, "CardinalPoint"),
296 (cardinal_end_point, "CardinalEndPoint"),
297 ] {
298 if let Some(value) = point.filter(|value| !(1..=9).contains(value)) {
299 return Err(invalid(ENTITY, attribute, value.to_string()));
300 }
301 }
302 let record = release.record(
303 ENTITY,
304 vec![
305 ("ForProfileSet", Value::Ref(for_profile_set)),
306 (
307 "CardinalPoint",
308 cardinal_point.map_or(Value::Null, Value::Integer),
309 ),
310 (
311 "ReferenceExtent",
312 reference_extent.map_or(Value::Null, Value::Real),
313 ),
314 ("ForProfileEndSet", Value::Ref(for_profile_end_set)),
315 (
316 "CardinalEndPoint",
317 cardinal_end_point.map_or(Value::Null, Value::Integer),
318 ),
319 ],
320 )?;
321 Ok(tx.create(record))
322}