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)]
184#[non_exhaustive]
185pub struct BlobTextureDraft<'a> {
186 pub repeat_s: bool,
188 pub repeat_t: bool,
190 pub mode: Option<&'a str>,
192 pub texture_transform: Option<EntityId>,
194 pub parameter: &'a [&'a str],
196 pub raster_format: &'a str,
198 pub raster_code: &'a str,
200}
201
202const RASTER_FORMATS: &[&str] = &["BMP", "JPG", "GIF", "PNG"];
206
207pub fn create_blob_texture(
220 tx: &mut Transaction,
221 schema: &Schema,
222 draft: BlobTextureDraft<'_>,
223) -> StyleResult<EntityId> {
224 let BlobTextureDraft {
225 repeat_s,
226 repeat_t,
227 mode,
228 texture_transform,
229 parameter,
230 raster_format,
231 raster_code,
232 } = draft;
233 const ENTITY: &str = "IfcBlobTexture";
234 if !RASTER_FORMATS.contains(&raster_format) {
235 return Err(invalid_authoring(ENTITY, "RasterFormat", raster_format));
236 }
237 if raster_code.is_empty() {
238 return Err(invalid_authoring(ENTITY, "RasterCode", "empty"));
239 }
240 if raster_code.len() % 2 != 0 {
243 return Err(invalid_authoring(ENTITY, "RasterCode", raster_code.len()));
244 }
245 if let Some(bad) = raster_code.chars().find(|c| !c.is_ascii_hexdigit()) {
246 return Err(invalid_authoring(ENTITY, "RasterCode", bad));
247 }
248 Ok(tx.create(build_named(
249 schema,
250 ENTITY,
251 blob_values(
252 repeat_s,
253 repeat_t,
254 mode,
255 texture_transform,
256 parameter,
257 raster_format,
258 raster_code,
259 )?,
260 )?))
261}
262
263#[allow(clippy::too_many_arguments)]
264fn blob_values(
265 repeat_s: bool,
266 repeat_t: bool,
267 mode: Option<&str>,
268 texture_transform: Option<EntityId>,
269 parameter: &[&str],
270 raster_format: &str,
271 raster_code: &str,
272) -> StyleResult<Vec<(&'static str, Value)>> {
273 const ENTITY: &str = "IfcBlobTexture";
274 let parameters = if parameter.is_empty() {
277 Value::Null
278 } else {
279 for text in parameter {
280 if text.trim().is_empty() {
281 return Err(invalid_authoring(ENTITY, "Parameter", "blank"));
282 }
283 }
284 Value::List(parameter.iter().map(|t| Value::Text((*t).into())).collect())
285 };
286 let mut values = vec![
287 ("RepeatS", Value::Bool(repeat_s)),
288 ("RepeatT", Value::Bool(repeat_t)),
289 ("RasterFormat", Value::Text(raster_format.into())),
290 ("RasterCode", Value::Binary(raster_code.into())),
291 ];
292 if !matches!(parameters, Value::Null) {
293 values.push(("Parameter", parameters));
294 }
295 optional_text(&mut values, "Mode", mode);
296 optional_reference(&mut values, "TextureTransform", texture_transform);
297 Ok(values)
298}
299
300pub fn create_surface_style_with_textures(
307 tx: &mut Transaction,
308 schema: &Schema,
309 textures: &[EntityId],
310) -> StyleResult<EntityId> {
311 const ENTITY: &str = "IfcSurfaceStyleWithTextures";
312 if textures.is_empty() {
313 return Err(invalid_authoring(ENTITY, "Textures", "empty"));
314 }
315 let values = vec![(
316 "Textures",
317 Value::List(textures.iter().copied().map(Value::Ref).collect()),
318 )];
319 Ok(tx.create(build_named(schema, ENTITY, values)?))
320}
321
322pub fn create_indexed_polygonal_texture_map(
329 tx: &mut Transaction,
330 schema: &Schema,
331 maps: &[EntityId],
332 mapped_to: EntityId,
333 tex_coords: EntityId,
334 tex_coord_indices: &[EntityId],
335) -> StyleResult<EntityId> {
336 const ENTITY: &str = "IfcIndexedPolygonalTextureMap";
337 if maps.is_empty() {
338 return Err(invalid_authoring(ENTITY, "Maps", "empty"));
339 }
340 if tex_coord_indices.is_empty() {
341 return Err(invalid_authoring(ENTITY, "TexCoordIndices", "empty"));
342 }
343 let values = vec![
344 (
345 "Maps",
346 Value::List(maps.iter().copied().map(Value::Ref).collect()),
347 ),
348 ("MappedTo", Value::Ref(mapped_to)),
349 ("TexCoords", Value::Ref(tex_coords)),
350 (
351 "TexCoordIndices",
352 Value::List(tex_coord_indices.iter().copied().map(Value::Ref).collect()),
353 ),
354 ];
355 Ok(tx.create(build_named(schema, ENTITY, values)?))
356}
357
358const TEXT_ALIGNMENT: &[&str] = &["left", "right", "center", "justify"];
360
361const TEXT_DECORATION: &[&str] = &["none", "underline", "overline", "line-through", "blink"];
363
364const TEXT_TRANSFORMATION: &[&str] = &["capitalize", "uppercase", "lowercase", "none"];
366
367fn closed(
368 entity: &'static str,
369 attribute: &'static str,
370 value: Option<&str>,
371 permitted: &[&str],
372) -> StyleResult<Value> {
373 let Some(text) = value else {
374 return Ok(Value::Null);
375 };
376 if !permitted.contains(&text) {
378 return Err(invalid_authoring(entity, attribute, text));
379 }
380 Ok(Value::Text(text.into()))
381}
382
383#[derive(Debug, Clone, Copy, PartialEq)]
389pub enum SizeValue<'a> {
390 Descriptive(&'a str),
392 Length(f64),
394 PositiveLength(f64),
396 Ratio(f64),
398 NormalisedRatio(f64),
400 PositiveRatio(f64),
402}
403
404impl SizeValue<'_> {
405 fn into_value(self, entity: &'static str, attribute: &'static str) -> StyleResult<Value> {
406 let (type_name, inner) = match self {
407 Self::Descriptive(text) => {
408 if text.trim().is_empty() {
409 return Err(invalid_authoring(entity, attribute, "blank"));
410 }
411 ("IFCDESCRIPTIVEMEASURE", Value::Text(text.into()))
412 }
413 Self::Length(v) => ("IFCLENGTHMEASURE", finite(entity, attribute, v)?),
414 Self::PositiveLength(v) => {
415 if v <= 0.0 {
416 return Err(invalid_authoring(entity, attribute, v));
417 }
418 ("IFCPOSITIVELENGTHMEASURE", finite(entity, attribute, v)?)
419 }
420 Self::Ratio(v) => ("IFCRATIOMEASURE", finite(entity, attribute, v)?),
421 Self::NormalisedRatio(v) => {
422 if !(0.0..=1.0).contains(&v) {
423 return Err(invalid_authoring(entity, attribute, v));
424 }
425 ("IFCNORMALISEDRATIOMEASURE", finite(entity, attribute, v)?)
426 }
427 Self::PositiveRatio(v) => {
428 if v <= 0.0 {
429 return Err(invalid_authoring(entity, attribute, v));
430 }
431 ("IFCPOSITIVERATIOMEASURE", finite(entity, attribute, v)?)
432 }
433 };
434 Ok(Value::Typed {
435 type_name: type_name.into(),
436 value: Box::new(inner),
437 })
438 }
439}
440
441fn finite(entity: &'static str, attribute: &'static str, value: f64) -> StyleResult<Value> {
442 if !value.is_finite() {
443 return Err(invalid_authoring(entity, attribute, value));
444 }
445 Ok(Value::Real(value))
446}
447
448#[derive(Debug, Clone, Copy, Default)]
450#[non_exhaustive]
451pub struct TextModelDraft<'a> {
452 pub text_indent: Option<SizeValue<'a>>,
454 pub text_align: Option<&'a str>,
456 pub text_decoration: Option<&'a str>,
458 pub letter_spacing: Option<SizeValue<'a>>,
460 pub word_spacing: Option<SizeValue<'a>>,
462 pub text_transform: Option<&'a str>,
464 pub line_height: Option<SizeValue<'a>>,
466}
467
468pub fn create_text_style_text_model(
479 tx: &mut Transaction,
480 schema: &Schema,
481 draft: TextModelDraft<'_>,
482) -> StyleResult<EntityId> {
483 const ENTITY: &str = "IfcTextStyleTextModel";
484 let values = vec![
485 ("TextIndent", size(ENTITY, "TextIndent", draft.text_indent)?),
486 (
487 "TextAlign",
488 closed(ENTITY, "TextAlign", draft.text_align, TEXT_ALIGNMENT)?,
489 ),
490 (
491 "TextDecoration",
492 closed(
493 ENTITY,
494 "TextDecoration",
495 draft.text_decoration,
496 TEXT_DECORATION,
497 )?,
498 ),
499 (
500 "LetterSpacing",
501 size(ENTITY, "LetterSpacing", draft.letter_spacing)?,
502 ),
503 (
504 "WordSpacing",
505 size(ENTITY, "WordSpacing", draft.word_spacing)?,
506 ),
507 (
508 "TextTransform",
509 closed(
510 ENTITY,
511 "TextTransform",
512 draft.text_transform,
513 TEXT_TRANSFORMATION,
514 )?,
515 ),
516 ("LineHeight", size(ENTITY, "LineHeight", draft.line_height)?),
517 ];
518 Ok(tx.create(build_named(schema, ENTITY, values)?))
519}
520
521fn size(
522 entity: &'static str,
523 attribute: &'static str,
524 value: Option<SizeValue<'_>>,
525) -> StyleResult<Value> {
526 value.map_or(Ok(Value::Null), |v| v.into_value(entity, attribute))
527}
528
529pub fn create_text_style(
539 tx: &mut Transaction,
540 schema: &Schema,
541 name: Option<&str>,
542 text_character_appearance: Option<EntityId>,
543 text_style: Option<EntityId>,
544 text_font_style: EntityId,
545 model_or_draughting: Option<bool>,
546) -> StyleResult<EntityId> {
547 const ENTITY: &str = "IfcTextStyle";
548 if name.is_some_and(|text| text.trim().is_empty()) {
549 return Err(invalid_authoring(ENTITY, "Name", "blank"));
550 }
551 let mut values = vec![("TextFontStyle", Value::Ref(text_font_style))];
552 optional_text(&mut values, "Name", name);
553 optional_reference(
554 &mut values,
555 "TextCharacterAppearance",
556 text_character_appearance,
557 );
558 optional_reference(&mut values, "TextStyle", text_style);
559 if let Some(flag) = model_or_draughting {
560 values.push(("ModelOrDraughting", Value::Bool(flag)));
561 }
562 Ok(tx.create(build_named(schema, ENTITY, values)?))
563}
564
565pub fn create_indexed_triangle_texture_map(
578 tx: &mut Transaction,
579 schema: &Schema,
580 maps: &[EntityId],
581 mapped_to: EntityId,
582 tex_coords: EntityId,
583 tex_coord_index: &[[i64; 3]],
584) -> StyleResult<EntityId> {
585 const ENTITY: &str = "IfcIndexedTriangleTextureMap";
586 if maps.is_empty() {
587 return Err(invalid_authoring(ENTITY, "Maps", "empty"));
588 }
589 for triangle in tex_coord_index {
590 if let Some(index) = triangle.iter().find(|value| **value < 1) {
591 return Err(invalid_authoring(
592 ENTITY,
593 "TexCoordIndex",
594 format!("{index} is not a positive one-based index"),
595 ));
596 }
597 }
598 let mut values = vec![
599 (
600 "Maps",
601 Value::List(maps.iter().copied().map(Value::Ref).collect()),
602 ),
603 ("MappedTo", Value::Ref(mapped_to)),
604 ("TexCoords", Value::Ref(tex_coords)),
605 ];
606 if !tex_coord_index.is_empty() {
607 values.push((
608 "TexCoordIndex",
609 Value::List(
610 tex_coord_index
611 .iter()
612 .map(|triangle| {
613 Value::List(triangle.iter().copied().map(Value::Integer).collect())
614 })
615 .collect(),
616 ),
617 ));
618 }
619 Ok(tx.create(build_named(schema, ENTITY, values)?))
620}
621
622pub fn create_light_distribution_data(
635 tx: &mut Transaction,
636 schema: &Schema,
637 main_plane_angle: f64,
638 secondary_plane_angles: &[f64],
639 luminous_intensities: &[f64],
640) -> StyleResult<EntityId> {
641 const ENTITY: &str = "IfcLightDistributionData";
642 if secondary_plane_angles.is_empty() || luminous_intensities.is_empty() {
643 return Err(invalid_authoring(ENTITY, "SecondaryPlaneAngle", "empty"));
644 }
645 if secondary_plane_angles.len() != luminous_intensities.len() {
646 return Err(invalid_authoring(
647 ENTITY,
648 "LuminousIntensity",
649 "expected one intensity per secondary angle",
650 ));
651 }
652 if !main_plane_angle.is_finite()
653 || secondary_plane_angles.iter().any(|v| !v.is_finite())
654 || luminous_intensities.iter().any(|v| !v.is_finite())
655 {
656 return Err(invalid_authoring(ENTITY, "MainPlaneAngle", "not finite"));
657 }
658 let reals = |values: &[f64]| Value::List(values.iter().copied().map(Value::Real).collect());
659 let values = vec![
660 ("MainPlaneAngle", Value::Real(main_plane_angle)),
661 ("SecondaryPlaneAngle", reals(secondary_plane_angles)),
662 ("LuminousIntensity", reals(luminous_intensities)),
663 ];
664 Ok(tx.create(build_named(schema, ENTITY, values)?))
665}
666
667pub fn create_light_intensity_distribution(
674 tx: &mut Transaction,
675 schema: &Schema,
676 light_distribution_curve: &str,
677 distribution_data: &[EntityId],
678) -> StyleResult<EntityId> {
679 const ENTITY: &str = "IfcLightIntensityDistribution";
680 if distribution_data.is_empty() {
681 return Err(invalid_authoring(ENTITY, "DistributionData", "empty"));
682 }
683 let declared = schema
684 .attributes(ENTITY)
685 .iter()
686 .find(|a| a.name.eq_ignore_ascii_case("LightDistributionCurve"))
687 .and_then(|a| schema.type_def(&a.type_name))
688 .is_some_and(|def| match &def.kind {
689 ifc_schema::TypeKind::Enumeration(values) => values
690 .iter()
691 .any(|v| v.eq_ignore_ascii_case(light_distribution_curve)),
692 _ => false,
693 });
694 if !declared {
695 return Err(invalid_authoring(
696 ENTITY,
697 "LightDistributionCurve",
698 light_distribution_curve,
699 ));
700 }
701 let values = vec![
702 (
703 "LightDistributionCurve",
704 Value::Enum(light_distribution_curve.into()),
705 ),
706 (
707 "DistributionData",
708 Value::List(distribution_data.iter().copied().map(Value::Ref).collect()),
709 ),
710 ];
711 Ok(tx.create(build_named(schema, ENTITY, values)?))
712}
713
714mod builders;