1use ifc_model::{EntityId, Transaction, Value};
26use ifc_schema::Schema;
27
28use crate::error::StyleResult;
29
30use super::{build_named, invalid_authoring, optional_reference, optional_text};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ExternalStyleKind {
38 HatchStyle,
40 SurfaceStyle,
42 TextFont,
44}
45
46impl ExternalStyleKind {
47 fn type_name(self) -> &'static str {
48 match self {
49 Self::HatchStyle => "IfcExternallyDefinedHatchStyle",
50 Self::SurfaceStyle => "IfcExternallyDefinedSurfaceStyle",
51 Self::TextFont => "IfcExternallyDefinedTextFont",
52 }
53 }
54}
55
56pub fn create_externally_defined_style(
68 tx: &mut Transaction,
69 schema: &Schema,
70 kind: ExternalStyleKind,
71 location: Option<&str>,
72 identification: Option<&str>,
73 name: Option<&str>,
74) -> StyleResult<EntityId> {
75 let entity = kind.type_name();
76 for (attribute, value) in [
77 ("Location", location),
78 ("Identification", identification),
79 ("Name", name),
80 ] {
81 if value.is_some_and(|text| text.trim().is_empty()) {
82 return Err(invalid_authoring(entity, attribute, "blank"));
83 }
84 }
85 if location.is_none() && identification.is_none() && name.is_none() {
86 return Err(invalid_authoring(entity, "Location", "no attribute set"));
87 }
88 let mut values = Vec::new();
89 optional_text(&mut values, "Location", location);
90 optional_text(&mut values, "Identification", identification);
91 optional_text(&mut values, "Name", name);
92 Ok(tx.create(build_named(schema, entity, values)?))
93}
94
95fn index_list(
96 entity: &'static str,
97 attribute: &'static str,
98 indices: &[i64],
99) -> StyleResult<Value> {
100 if indices.len() < 3 {
101 return Err(invalid_authoring(entity, attribute, indices.len()));
102 }
103 if let Some(bad) = indices.iter().find(|index| **index < 1) {
106 return Err(invalid_authoring(entity, attribute, bad));
107 }
108 Ok(Value::List(
109 indices.iter().copied().map(Value::Integer).collect(),
110 ))
111}
112
113pub fn create_texture_coordinate_indices(
119 tx: &mut Transaction,
120 schema: &Schema,
121 tex_coord_index: &[i64],
122 tex_coords_of: EntityId,
123) -> StyleResult<EntityId> {
124 const ENTITY: &str = "IfcTextureCoordinateIndices";
125 let values = vec![
126 (
127 "TexCoordIndex",
128 index_list(ENTITY, "TexCoordIndex", tex_coord_index)?,
129 ),
130 ("TexCoordsOf", Value::Ref(tex_coords_of)),
131 ];
132 Ok(tx.create(build_named(schema, ENTITY, values)?))
133}
134
135pub fn create_texture_coordinate_indices_with_voids(
149 tx: &mut Transaction,
150 schema: &Schema,
151 tex_coord_index: &[i64],
152 tex_coords_of: EntityId,
153 inner_tex_coord_indices: &[&[i64]],
154) -> StyleResult<EntityId> {
155 const ENTITY: &str = "IfcTextureCoordinateIndicesWithVoids";
156 if inner_tex_coord_indices.is_empty() {
157 return Err(invalid_authoring(ENTITY, "InnerTexCoordIndices", "empty"));
158 }
159 let mut inner = Vec::with_capacity(inner_tex_coord_indices.len());
160 for void in inner_tex_coord_indices {
161 let list = index_list(ENTITY, "InnerTexCoordIndices", void)?;
162 for (position, index) in void.iter().enumerate() {
165 if void[..position].contains(index) {
166 return Err(invalid_authoring(ENTITY, "InnerTexCoordIndices", index));
167 }
168 }
169 inner.push(list);
170 }
171 let values = vec![
172 (
173 "TexCoordIndex",
174 index_list(ENTITY, "TexCoordIndex", tex_coord_index)?,
175 ),
176 ("TexCoordsOf", Value::Ref(tex_coords_of)),
177 ("InnerTexCoordIndices", Value::List(inner)),
178 ];
179 Ok(tx.create(build_named(schema, ENTITY, values)?))
180}
181
182#[derive(Debug, Clone, Copy, Default)]
184pub struct BlobTextureDraft<'a> {
185 pub repeat_s: bool,
187 pub repeat_t: bool,
189 pub mode: Option<&'a str>,
191 pub texture_transform: Option<EntityId>,
193 pub parameter: &'a [&'a str],
195 pub raster_format: &'a str,
197 pub raster_code: &'a str,
199}
200const RASTER_FORMATS: &[&str] = &["BMP", "JPG", "GIF", "PNG"];
204
205pub fn create_blob_texture(
218 tx: &mut Transaction,
219 schema: &Schema,
220 draft: BlobTextureDraft<'_>,
221) -> StyleResult<EntityId> {
222 let BlobTextureDraft {
223 repeat_s,
224 repeat_t,
225 mode,
226 texture_transform,
227 parameter,
228 raster_format,
229 raster_code,
230 } = draft;
231 const ENTITY: &str = "IfcBlobTexture";
232 if !RASTER_FORMATS.contains(&raster_format) {
233 return Err(invalid_authoring(ENTITY, "RasterFormat", raster_format));
234 }
235 if raster_code.is_empty() {
236 return Err(invalid_authoring(ENTITY, "RasterCode", "empty"));
237 }
238 if raster_code.len() % 2 != 0 {
241 return Err(invalid_authoring(ENTITY, "RasterCode", raster_code.len()));
242 }
243 if let Some(bad) = raster_code.chars().find(|c| !c.is_ascii_hexdigit()) {
244 return Err(invalid_authoring(ENTITY, "RasterCode", bad));
245 }
246 Ok(tx.create(build_named(
247 schema,
248 ENTITY,
249 blob_values(
250 repeat_s,
251 repeat_t,
252 mode,
253 texture_transform,
254 parameter,
255 raster_format,
256 raster_code,
257 )?,
258 )?))
259}
260
261#[allow(clippy::too_many_arguments)]
262fn blob_values(
263 repeat_s: bool,
264 repeat_t: bool,
265 mode: Option<&str>,
266 texture_transform: Option<EntityId>,
267 parameter: &[&str],
268 raster_format: &str,
269 raster_code: &str,
270) -> StyleResult<Vec<(&'static str, Value)>> {
271 const ENTITY: &str = "IfcBlobTexture";
272 let parameters = if parameter.is_empty() {
275 Value::Null
276 } else {
277 for text in parameter {
278 if text.trim().is_empty() {
279 return Err(invalid_authoring(ENTITY, "Parameter", "blank"));
280 }
281 }
282 Value::List(parameter.iter().map(|t| Value::Text((*t).into())).collect())
283 };
284 let mut values = vec![
285 ("RepeatS", Value::Bool(repeat_s)),
286 ("RepeatT", Value::Bool(repeat_t)),
287 ("RasterFormat", Value::Text(raster_format.into())),
288 ("RasterCode", Value::Binary(raster_code.into())),
289 ];
290 if !matches!(parameters, Value::Null) {
291 values.push(("Parameter", parameters));
292 }
293 optional_text(&mut values, "Mode", mode);
294 optional_reference(&mut values, "TextureTransform", texture_transform);
295 Ok(values)
296}
297
298pub fn create_surface_style_with_textures(
305 tx: &mut Transaction,
306 schema: &Schema,
307 textures: &[EntityId],
308) -> StyleResult<EntityId> {
309 const ENTITY: &str = "IfcSurfaceStyleWithTextures";
310 if textures.is_empty() {
311 return Err(invalid_authoring(ENTITY, "Textures", "empty"));
312 }
313 let values = vec![(
314 "Textures",
315 Value::List(textures.iter().copied().map(Value::Ref).collect()),
316 )];
317 Ok(tx.create(build_named(schema, ENTITY, values)?))
318}
319
320pub fn create_indexed_polygonal_texture_map(
327 tx: &mut Transaction,
328 schema: &Schema,
329 maps: &[EntityId],
330 mapped_to: EntityId,
331 tex_coords: EntityId,
332 tex_coord_indices: &[EntityId],
333) -> StyleResult<EntityId> {
334 const ENTITY: &str = "IfcIndexedPolygonalTextureMap";
335 if maps.is_empty() {
336 return Err(invalid_authoring(ENTITY, "Maps", "empty"));
337 }
338 if tex_coord_indices.is_empty() {
339 return Err(invalid_authoring(ENTITY, "TexCoordIndices", "empty"));
340 }
341 let values = vec![
342 (
343 "Maps",
344 Value::List(maps.iter().copied().map(Value::Ref).collect()),
345 ),
346 ("MappedTo", Value::Ref(mapped_to)),
347 ("TexCoords", Value::Ref(tex_coords)),
348 (
349 "TexCoordIndices",
350 Value::List(tex_coord_indices.iter().copied().map(Value::Ref).collect()),
351 ),
352 ];
353 Ok(tx.create(build_named(schema, ENTITY, values)?))
354}
355
356const TEXT_ALIGNMENT: &[&str] = &["left", "right", "center", "justify"];
358
359const TEXT_DECORATION: &[&str] = &["none", "underline", "overline", "line-through", "blink"];
361
362const TEXT_TRANSFORMATION: &[&str] = &["capitalize", "uppercase", "lowercase", "none"];
364
365fn closed(
366 entity: &'static str,
367 attribute: &'static str,
368 value: Option<&str>,
369 permitted: &[&str],
370) -> StyleResult<Value> {
371 let Some(text) = value else {
372 return Ok(Value::Null);
373 };
374 if !permitted.contains(&text) {
376 return Err(invalid_authoring(entity, attribute, text));
377 }
378 Ok(Value::Text(text.into()))
379}
380
381#[derive(Debug, Clone, Copy, PartialEq)]
387pub enum SizeValue<'a> {
388 Descriptive(&'a str),
390 Length(f64),
392 PositiveLength(f64),
394 Ratio(f64),
396 NormalisedRatio(f64),
398 PositiveRatio(f64),
400}
401
402impl SizeValue<'_> {
403 fn into_value(self, entity: &'static str, attribute: &'static str) -> StyleResult<Value> {
404 let (type_name, inner) = match self {
405 Self::Descriptive(text) => {
406 if text.trim().is_empty() {
407 return Err(invalid_authoring(entity, attribute, "blank"));
408 }
409 ("IFCDESCRIPTIVEMEASURE", Value::Text(text.into()))
410 }
411 Self::Length(v) => ("IFCLENGTHMEASURE", finite(entity, attribute, v)?),
412 Self::PositiveLength(v) => {
413 if v <= 0.0 {
414 return Err(invalid_authoring(entity, attribute, v));
415 }
416 ("IFCPOSITIVELENGTHMEASURE", finite(entity, attribute, v)?)
417 }
418 Self::Ratio(v) => ("IFCRATIOMEASURE", finite(entity, attribute, v)?),
419 Self::NormalisedRatio(v) => {
420 if !(0.0..=1.0).contains(&v) {
421 return Err(invalid_authoring(entity, attribute, v));
422 }
423 ("IFCNORMALISEDRATIOMEASURE", finite(entity, attribute, v)?)
424 }
425 Self::PositiveRatio(v) => {
426 if v <= 0.0 {
427 return Err(invalid_authoring(entity, attribute, v));
428 }
429 ("IFCPOSITIVERATIOMEASURE", finite(entity, attribute, v)?)
430 }
431 };
432 Ok(Value::Typed {
433 type_name: type_name.into(),
434 value: Box::new(inner),
435 })
436 }
437}
438
439fn finite(entity: &'static str, attribute: &'static str, value: f64) -> StyleResult<Value> {
440 if !value.is_finite() {
441 return Err(invalid_authoring(entity, attribute, value));
442 }
443 Ok(Value::Real(value))
444}
445
446#[derive(Debug, Clone, Copy, Default)]
448pub struct TextModelDraft<'a> {
449 pub text_indent: Option<SizeValue<'a>>,
451 pub text_align: Option<&'a str>,
453 pub text_decoration: Option<&'a str>,
455 pub letter_spacing: Option<SizeValue<'a>>,
457 pub word_spacing: Option<SizeValue<'a>>,
459 pub text_transform: Option<&'a str>,
461 pub line_height: Option<SizeValue<'a>>,
463}
464
465pub fn create_text_style_text_model(
476 tx: &mut Transaction,
477 schema: &Schema,
478 draft: TextModelDraft<'_>,
479) -> StyleResult<EntityId> {
480 const ENTITY: &str = "IfcTextStyleTextModel";
481 let values = vec![
482 ("TextIndent", size(ENTITY, "TextIndent", draft.text_indent)?),
483 (
484 "TextAlign",
485 closed(ENTITY, "TextAlign", draft.text_align, TEXT_ALIGNMENT)?,
486 ),
487 (
488 "TextDecoration",
489 closed(
490 ENTITY,
491 "TextDecoration",
492 draft.text_decoration,
493 TEXT_DECORATION,
494 )?,
495 ),
496 (
497 "LetterSpacing",
498 size(ENTITY, "LetterSpacing", draft.letter_spacing)?,
499 ),
500 (
501 "WordSpacing",
502 size(ENTITY, "WordSpacing", draft.word_spacing)?,
503 ),
504 (
505 "TextTransform",
506 closed(
507 ENTITY,
508 "TextTransform",
509 draft.text_transform,
510 TEXT_TRANSFORMATION,
511 )?,
512 ),
513 ("LineHeight", size(ENTITY, "LineHeight", draft.line_height)?),
514 ];
515 Ok(tx.create(build_named(schema, ENTITY, values)?))
516}
517
518fn size(
519 entity: &'static str,
520 attribute: &'static str,
521 value: Option<SizeValue<'_>>,
522) -> StyleResult<Value> {
523 value.map_or(Ok(Value::Null), |v| v.into_value(entity, attribute))
524}
525
526pub fn create_text_style(
536 tx: &mut Transaction,
537 schema: &Schema,
538 name: Option<&str>,
539 text_character_appearance: Option<EntityId>,
540 text_style: Option<EntityId>,
541 text_font_style: EntityId,
542 model_or_draughting: Option<bool>,
543) -> StyleResult<EntityId> {
544 const ENTITY: &str = "IfcTextStyle";
545 if name.is_some_and(|text| text.trim().is_empty()) {
546 return Err(invalid_authoring(ENTITY, "Name", "blank"));
547 }
548 let mut values = vec![("TextFontStyle", Value::Ref(text_font_style))];
549 optional_text(&mut values, "Name", name);
550 optional_reference(
551 &mut values,
552 "TextCharacterAppearance",
553 text_character_appearance,
554 );
555 optional_reference(&mut values, "TextStyle", text_style);
556 if let Some(flag) = model_or_draughting {
557 values.push(("ModelOrDraughting", Value::Bool(flag)));
558 }
559 Ok(tx.create(build_named(schema, ENTITY, values)?))
560}
561
562pub fn create_indexed_triangle_texture_map(
575 tx: &mut Transaction,
576 schema: &Schema,
577 maps: &[EntityId],
578 mapped_to: EntityId,
579 tex_coords: EntityId,
580 tex_coord_index: &[[i64; 3]],
581) -> StyleResult<EntityId> {
582 const ENTITY: &str = "IfcIndexedTriangleTextureMap";
583 if maps.is_empty() {
584 return Err(invalid_authoring(ENTITY, "Maps", "empty"));
585 }
586 for triangle in tex_coord_index {
587 if let Some(index) = triangle.iter().find(|value| **value < 1) {
588 return Err(invalid_authoring(
589 ENTITY,
590 "TexCoordIndex",
591 format!("{index} is not a positive one-based index"),
592 ));
593 }
594 }
595 let mut values = vec![
596 (
597 "Maps",
598 Value::List(maps.iter().copied().map(Value::Ref).collect()),
599 ),
600 ("MappedTo", Value::Ref(mapped_to)),
601 ("TexCoords", Value::Ref(tex_coords)),
602 ];
603 if !tex_coord_index.is_empty() {
604 values.push((
605 "TexCoordIndex",
606 Value::List(
607 tex_coord_index
608 .iter()
609 .map(|triangle| {
610 Value::List(triangle.iter().copied().map(Value::Integer).collect())
611 })
612 .collect(),
613 ),
614 ));
615 }
616 Ok(tx.create(build_named(schema, ENTITY, values)?))
617}
618
619pub fn create_light_distribution_data(
632 tx: &mut Transaction,
633 schema: &Schema,
634 main_plane_angle: f64,
635 secondary_plane_angles: &[f64],
636 luminous_intensities: &[f64],
637) -> StyleResult<EntityId> {
638 const ENTITY: &str = "IfcLightDistributionData";
639 if secondary_plane_angles.is_empty() || luminous_intensities.is_empty() {
640 return Err(invalid_authoring(ENTITY, "SecondaryPlaneAngle", "empty"));
641 }
642 if secondary_plane_angles.len() != luminous_intensities.len() {
643 return Err(invalid_authoring(
644 ENTITY,
645 "LuminousIntensity",
646 "expected one intensity per secondary angle",
647 ));
648 }
649 if !main_plane_angle.is_finite()
650 || secondary_plane_angles.iter().any(|v| !v.is_finite())
651 || luminous_intensities.iter().any(|v| !v.is_finite())
652 {
653 return Err(invalid_authoring(ENTITY, "MainPlaneAngle", "not finite"));
654 }
655 let reals = |values: &[f64]| Value::List(values.iter().copied().map(Value::Real).collect());
656 let values = vec![
657 ("MainPlaneAngle", Value::Real(main_plane_angle)),
658 ("SecondaryPlaneAngle", reals(secondary_plane_angles)),
659 ("LuminousIntensity", reals(luminous_intensities)),
660 ];
661 Ok(tx.create(build_named(schema, ENTITY, values)?))
662}
663
664pub fn create_light_intensity_distribution(
671 tx: &mut Transaction,
672 schema: &Schema,
673 light_distribution_curve: &str,
674 distribution_data: &[EntityId],
675) -> StyleResult<EntityId> {
676 const ENTITY: &str = "IfcLightIntensityDistribution";
677 if distribution_data.is_empty() {
678 return Err(invalid_authoring(ENTITY, "DistributionData", "empty"));
679 }
680 let declared = schema
681 .attributes(ENTITY)
682 .iter()
683 .find(|a| a.name.eq_ignore_ascii_case("LightDistributionCurve"))
684 .and_then(|a| schema.type_def(&a.type_name))
685 .is_some_and(|def| match &def.kind {
686 ifc_schema::TypeKind::Enumeration(values) => values
687 .iter()
688 .any(|v| v.eq_ignore_ascii_case(light_distribution_curve)),
689 _ => false,
690 });
691 if !declared {
692 return Err(invalid_authoring(
693 ENTITY,
694 "LightDistributionCurve",
695 light_distribution_curve,
696 ));
697 }
698 let values = vec![
699 (
700 "LightDistributionCurve",
701 Value::Enum(light_distribution_curve.into()),
702 ),
703 (
704 "DistributionData",
705 Value::List(distribution_data.iter().copied().map(Value::Ref).collect()),
706 ),
707 ];
708 Ok(tx.create(build_named(schema, ENTITY, values)?))
709}