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)]
17#[non_exhaustive]
18pub struct ConstituentDraft<'a> {
19 pub name: Option<&'a str>,
21 pub description: Option<&'a str>,
23 pub material: EntityId,
25 pub fraction: Option<f64>,
27 pub category: Option<&'a str>,
29}
30
31impl<'a> ConstituentDraft<'a> {
32 #[must_use]
34 pub const fn new(material: EntityId) -> Self {
35 Self {
36 name: None,
37 description: None,
38 material,
39 fraction: None,
40 category: None,
41 }
42 }
43
44 #[must_use]
46 pub const fn name(mut self, value: &'a str) -> Self {
47 self.name = Some(value);
48 self
49 }
50
51 #[must_use]
53 pub const fn description(mut self, value: &'a str) -> Self {
54 self.description = Some(value);
55 self
56 }
57
58 #[must_use]
60 pub const fn fraction(mut self, value: f64) -> Self {
61 self.fraction = 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
73pub fn create_constituent(
79 tx: &mut Transaction,
80 model: &Model,
81 draft: ConstituentDraft<'_>,
82) -> MaterialResult<EntityId> {
83 const ENTITY: &str = "IFCMATERIALCONSTITUENT";
84 let release = Release::of(model);
85 release.require_entity(ENTITY, None)?;
86 if let Some(fraction) = draft.fraction {
87 if !fraction.is_finite() || !(0.0..=1.0).contains(&fraction) {
88 return Err(invalid(
89 ENTITY,
90 "Fraction",
91 "expected a normalised ratio in 0..=1",
92 ));
93 }
94 }
95 require_type(tx, model, release, draft.material, &["IFCMATERIAL"])?;
96 let record = release.record(
97 ENTITY,
98 vec![
99 ("Name", optional_text(draft.name)),
100 ("Description", optional_text(draft.description)),
101 ("Material", Value::Ref(draft.material)),
102 ("Fraction", draft.fraction.map_or(Value::Null, Value::Real)),
103 ("Category", optional_text(draft.category)),
104 ],
105 )?;
106 Ok(tx.create(record))
107}
108
109pub fn create_constituent_set(
112 tx: &mut Transaction,
113 model: &Model,
114 constituents: &[EntityId],
115 name: Option<&str>,
116 description: Option<&str>,
117) -> MaterialResult<EntityId> {
118 const ENTITY: &str = "IFCMATERIALCONSTITUENTSET";
119 let release = Release::of(model);
120 release.require_entity(ENTITY, None)?;
121 if constituents.is_empty() {
122 return Err(invalid(
123 ENTITY,
124 "MaterialConstituents",
125 "expected at least one constituent",
126 ));
127 }
128 for &constituent in constituents {
129 require_type(tx, model, release, constituent, &["IFCMATERIALCONSTITUENT"])?;
130 }
131 let record = release.record(
132 ENTITY,
133 vec![
134 ("Name", optional_text(name)),
135 ("Description", optional_text(description)),
136 ("MaterialConstituents", refs(constituents)),
137 ],
138 )?;
139 Ok(tx.create(record))
140}
141
142#[derive(Debug, Clone, Copy)]
144#[non_exhaustive]
145pub struct ProfileDraft<'a> {
146 pub name: Option<&'a str>,
148 pub description: Option<&'a str>,
150 pub material: Option<EntityId>,
152 pub profile: EntityId,
154 pub priority: Option<i64>,
156 pub category: Option<&'a str>,
158}
159
160impl<'a> ProfileDraft<'a> {
161 #[must_use]
164 pub const fn new(profile: EntityId) -> Self {
165 Self {
166 name: None,
167 description: None,
168 material: None,
169 profile,
170 priority: None,
171 category: None,
172 }
173 }
174
175 #[must_use]
177 pub const fn name(mut self, value: &'a str) -> Self {
178 self.name = Some(value);
179 self
180 }
181
182 #[must_use]
184 pub const fn description(mut self, value: &'a str) -> Self {
185 self.description = Some(value);
186 self
187 }
188
189 #[must_use]
191 pub const fn material(mut self, value: EntityId) -> Self {
192 self.material = Some(value);
193 self
194 }
195
196 #[must_use]
198 pub const fn priority(mut self, value: i64) -> Self {
199 self.priority = Some(value);
200 self
201 }
202
203 #[must_use]
205 pub const fn category(mut self, value: &'a str) -> Self {
206 self.category = Some(value);
207 self
208 }
209}
210
211fn profile_values(
214 tx: &Transaction,
215 model: &Model,
216 release: Release<'_>,
217 entity: &'static str,
218 draft: &ProfileDraft<'_>,
219) -> MaterialResult<Vec<(&'static str, Value)>> {
220 release.require_entity(entity, None)?;
221 if let Some(priority) = draft.priority.filter(|value| !(0..=100).contains(value)) {
222 return Err(invalid(entity, "Priority", priority.to_string()));
223 }
224 if let Some(material) = draft.material {
225 require_type(tx, model, release, material, &["IFCMATERIAL"])?;
226 }
227 require_exists(tx, model, draft.profile)?;
228 Ok(vec![
229 ("Name", optional_text(draft.name)),
230 ("Description", optional_text(draft.description)),
231 ("Material", draft.material.map_or(Value::Null, Value::Ref)),
232 ("Profile", Value::Ref(draft.profile)),
233 (
234 "Priority",
235 draft.priority.map_or(Value::Null, Value::Integer),
236 ),
237 ("Category", optional_text(draft.category)),
238 ])
239}
240
241pub fn create_profile(
246 tx: &mut Transaction,
247 model: &Model,
248 draft: ProfileDraft<'_>,
249) -> MaterialResult<EntityId> {
250 const ENTITY: &str = "IFCMATERIALPROFILE";
251 let release = Release::of(model);
252 let values = profile_values(tx, model, release, ENTITY, &draft)?;
253 Ok(tx.create(release.record(ENTITY, values)?))
254}
255
256pub fn create_profile_set(
259 tx: &mut Transaction,
260 model: &Model,
261 profiles: &[EntityId],
262 name: Option<&str>,
263 description: Option<&str>,
264 composite_profile: Option<EntityId>,
265) -> MaterialResult<EntityId> {
266 const ENTITY: &str = "IFCMATERIALPROFILESET";
267 let release = Release::of(model);
268 release.require_entity(ENTITY, None)?;
269 if profiles.is_empty() {
270 return Err(invalid(
271 ENTITY,
272 "MaterialProfiles",
273 "expected at least one profile",
274 ));
275 }
276 for &profile in profiles {
277 require_type(tx, model, release, profile, &["IFCMATERIALPROFILE"])?;
278 }
279 if let Some(composite) = composite_profile {
280 require_exists(tx, model, composite)?;
281 }
282 let record = release.record(
283 ENTITY,
284 vec![
285 ("Name", optional_text(name)),
286 ("Description", optional_text(description)),
287 ("MaterialProfiles", refs(profiles)),
288 (
289 "CompositeProfile",
290 composite_profile.map_or(Value::Null, Value::Ref),
291 ),
292 ],
293 )?;
294 Ok(tx.create(record))
295}
296
297pub fn create_profile_set_usage(
302 tx: &mut Transaction,
303 model: &Model,
304 for_profile_set: EntityId,
305 cardinal_point: Option<i64>,
306 reference_extent: Option<f64>,
307) -> MaterialResult<EntityId> {
308 const ENTITY: &str = "IFCMATERIALPROFILESETUSAGE";
309 let release = Release::of(model);
310 release.require_entity(ENTITY, None)?;
311 require_type(
312 tx,
313 model,
314 release,
315 for_profile_set,
316 &["IFCMATERIALPROFILESET"],
317 )?;
318 if let Some(point) = cardinal_point.filter(|value| !(1..=9).contains(value)) {
319 return Err(invalid(ENTITY, "CardinalPoint", point.to_string()));
320 }
321 let record = release.record(
322 ENTITY,
323 vec![
324 ("ForProfileSet", Value::Ref(for_profile_set)),
325 (
326 "CardinalPoint",
327 cardinal_point.map_or(Value::Null, Value::Integer),
328 ),
329 (
330 "ReferenceExtent",
331 reference_extent.map_or(Value::Null, Value::Real),
332 ),
333 ],
334 )?;
335 Ok(tx.create(record))
336}
337
338pub fn create_profile_with_offsets(
349 tx: &mut Transaction,
350 model: &Model,
351 draft: ProfileDraft<'_>,
352 offset_values: [f64; 2],
353) -> MaterialResult<EntityId> {
354 const ENTITY: &str = "IFCMATERIALPROFILEWITHOFFSETS";
355 let release = Release::of(model);
356 release.require_entity(ENTITY, None)?;
357 if offset_values.iter().any(|value| !value.is_finite()) {
358 return Err(invalid(ENTITY, "OffsetValues", "expected finite lengths"));
359 }
360 let mut values = profile_values(tx, model, release, ENTITY, &draft)?;
361 values.push(("OffsetValues", reals(&offset_values)));
362 Ok(tx.create(release.record(ENTITY, values)?))
363}
364
365pub fn create_profile_set_usage_tapering(
374 tx: &mut Transaction,
375 model: &Model,
376 for_profile_set: EntityId,
377 for_profile_end_set: EntityId,
378 cardinal_point: Option<i64>,
379 cardinal_end_point: Option<i64>,
380 reference_extent: Option<f64>,
381) -> MaterialResult<EntityId> {
382 const ENTITY: &str = "IFCMATERIALPROFILESETUSAGETAPERING";
383 let release = Release::of(model);
384 release.require_entity(ENTITY, None)?;
385 for set in [for_profile_set, for_profile_end_set] {
386 require_type(tx, model, release, set, &["IFCMATERIALPROFILESET"])?;
387 }
388 for (point, attribute) in [
390 (cardinal_point, "CardinalPoint"),
391 (cardinal_end_point, "CardinalEndPoint"),
392 ] {
393 if let Some(value) = point.filter(|value| !(1..=9).contains(value)) {
394 return Err(invalid(ENTITY, attribute, value.to_string()));
395 }
396 }
397 let record = release.record(
398 ENTITY,
399 vec![
400 ("ForProfileSet", Value::Ref(for_profile_set)),
401 (
402 "CardinalPoint",
403 cardinal_point.map_or(Value::Null, Value::Integer),
404 ),
405 (
406 "ReferenceExtent",
407 reference_extent.map_or(Value::Null, Value::Real),
408 ),
409 ("ForProfileEndSet", Value::Ref(for_profile_end_set)),
410 (
411 "CardinalEndPoint",
412 cardinal_end_point.map_or(Value::Null, Value::Integer),
413 ),
414 ],
415 )?;
416 Ok(tx.create(record))
417}