use ifc_model::{EntityId, Transaction, Value};
use ifc_schema::Schema;
use crate::error::StyleResult;
use super::{build_named, invalid_authoring, optional_reference, optional_text};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExternalStyleKind {
HatchStyle,
SurfaceStyle,
TextFont,
}
impl ExternalStyleKind {
fn type_name(self) -> &'static str {
match self {
Self::HatchStyle => "IfcExternallyDefinedHatchStyle",
Self::SurfaceStyle => "IfcExternallyDefinedSurfaceStyle",
Self::TextFont => "IfcExternallyDefinedTextFont",
}
}
}
pub fn create_externally_defined_style(
tx: &mut Transaction,
schema: &Schema,
kind: ExternalStyleKind,
location: Option<&str>,
identification: Option<&str>,
name: Option<&str>,
) -> StyleResult<EntityId> {
let entity = kind.type_name();
for (attribute, value) in [
("Location", location),
("Identification", identification),
("Name", name),
] {
if value.is_some_and(|text| text.trim().is_empty()) {
return Err(invalid_authoring(entity, attribute, "blank"));
}
}
if location.is_none() && identification.is_none() && name.is_none() {
return Err(invalid_authoring(entity, "Location", "no attribute set"));
}
let mut values = Vec::new();
optional_text(&mut values, "Location", location);
optional_text(&mut values, "Identification", identification);
optional_text(&mut values, "Name", name);
Ok(tx.create(build_named(schema, entity, values)?))
}
fn index_list(
entity: &'static str,
attribute: &'static str,
indices: &[i64],
) -> StyleResult<Value> {
if indices.len() < 3 {
return Err(invalid_authoring(entity, attribute, indices.len()));
}
if let Some(bad) = indices.iter().find(|index| **index < 1) {
return Err(invalid_authoring(entity, attribute, bad));
}
Ok(Value::List(
indices.iter().copied().map(Value::Integer).collect(),
))
}
pub fn create_texture_coordinate_indices(
tx: &mut Transaction,
schema: &Schema,
tex_coord_index: &[i64],
tex_coords_of: EntityId,
) -> StyleResult<EntityId> {
const ENTITY: &str = "IfcTextureCoordinateIndices";
let values = vec![
(
"TexCoordIndex",
index_list(ENTITY, "TexCoordIndex", tex_coord_index)?,
),
("TexCoordsOf", Value::Ref(tex_coords_of)),
];
Ok(tx.create(build_named(schema, ENTITY, values)?))
}
pub fn create_texture_coordinate_indices_with_voids(
tx: &mut Transaction,
schema: &Schema,
tex_coord_index: &[i64],
tex_coords_of: EntityId,
inner_tex_coord_indices: &[&[i64]],
) -> StyleResult<EntityId> {
const ENTITY: &str = "IfcTextureCoordinateIndicesWithVoids";
if inner_tex_coord_indices.is_empty() {
return Err(invalid_authoring(ENTITY, "InnerTexCoordIndices", "empty"));
}
let mut inner = Vec::with_capacity(inner_tex_coord_indices.len());
for void in inner_tex_coord_indices {
let list = index_list(ENTITY, "InnerTexCoordIndices", void)?;
for (position, index) in void.iter().enumerate() {
if void[..position].contains(index) {
return Err(invalid_authoring(ENTITY, "InnerTexCoordIndices", index));
}
}
inner.push(list);
}
let values = vec![
(
"TexCoordIndex",
index_list(ENTITY, "TexCoordIndex", tex_coord_index)?,
),
("TexCoordsOf", Value::Ref(tex_coords_of)),
("InnerTexCoordIndices", Value::List(inner)),
];
Ok(tx.create(build_named(schema, ENTITY, values)?))
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct BlobTextureDraft<'a> {
pub repeat_s: bool,
pub repeat_t: bool,
pub mode: Option<&'a str>,
pub texture_transform: Option<EntityId>,
pub parameter: &'a [&'a str],
pub raster_format: &'a str,
pub raster_code: &'a str,
}
const RASTER_FORMATS: &[&str] = &["BMP", "JPG", "GIF", "PNG"];
pub fn create_blob_texture(
tx: &mut Transaction,
schema: &Schema,
draft: BlobTextureDraft<'_>,
) -> StyleResult<EntityId> {
let BlobTextureDraft {
repeat_s,
repeat_t,
mode,
texture_transform,
parameter,
raster_format,
raster_code,
} = draft;
const ENTITY: &str = "IfcBlobTexture";
if !RASTER_FORMATS.contains(&raster_format) {
return Err(invalid_authoring(ENTITY, "RasterFormat", raster_format));
}
if raster_code.is_empty() {
return Err(invalid_authoring(ENTITY, "RasterCode", "empty"));
}
if raster_code.len() % 2 != 0 {
return Err(invalid_authoring(ENTITY, "RasterCode", raster_code.len()));
}
if let Some(bad) = raster_code.chars().find(|c| !c.is_ascii_hexdigit()) {
return Err(invalid_authoring(ENTITY, "RasterCode", bad));
}
Ok(tx.create(build_named(
schema,
ENTITY,
blob_values(
repeat_s,
repeat_t,
mode,
texture_transform,
parameter,
raster_format,
raster_code,
)?,
)?))
}
#[allow(clippy::too_many_arguments)]
fn blob_values(
repeat_s: bool,
repeat_t: bool,
mode: Option<&str>,
texture_transform: Option<EntityId>,
parameter: &[&str],
raster_format: &str,
raster_code: &str,
) -> StyleResult<Vec<(&'static str, Value)>> {
const ENTITY: &str = "IfcBlobTexture";
let parameters = if parameter.is_empty() {
Value::Null
} else {
for text in parameter {
if text.trim().is_empty() {
return Err(invalid_authoring(ENTITY, "Parameter", "blank"));
}
}
Value::List(parameter.iter().map(|t| Value::Text((*t).into())).collect())
};
let mut values = vec![
("RepeatS", Value::Bool(repeat_s)),
("RepeatT", Value::Bool(repeat_t)),
("RasterFormat", Value::Text(raster_format.into())),
("RasterCode", Value::Binary(raster_code.into())),
];
if !matches!(parameters, Value::Null) {
values.push(("Parameter", parameters));
}
optional_text(&mut values, "Mode", mode);
optional_reference(&mut values, "TextureTransform", texture_transform);
Ok(values)
}
pub fn create_surface_style_with_textures(
tx: &mut Transaction,
schema: &Schema,
textures: &[EntityId],
) -> StyleResult<EntityId> {
const ENTITY: &str = "IfcSurfaceStyleWithTextures";
if textures.is_empty() {
return Err(invalid_authoring(ENTITY, "Textures", "empty"));
}
let values = vec![(
"Textures",
Value::List(textures.iter().copied().map(Value::Ref).collect()),
)];
Ok(tx.create(build_named(schema, ENTITY, values)?))
}
pub fn create_indexed_polygonal_texture_map(
tx: &mut Transaction,
schema: &Schema,
maps: &[EntityId],
mapped_to: EntityId,
tex_coords: EntityId,
tex_coord_indices: &[EntityId],
) -> StyleResult<EntityId> {
const ENTITY: &str = "IfcIndexedPolygonalTextureMap";
if maps.is_empty() {
return Err(invalid_authoring(ENTITY, "Maps", "empty"));
}
if tex_coord_indices.is_empty() {
return Err(invalid_authoring(ENTITY, "TexCoordIndices", "empty"));
}
let values = vec![
(
"Maps",
Value::List(maps.iter().copied().map(Value::Ref).collect()),
),
("MappedTo", Value::Ref(mapped_to)),
("TexCoords", Value::Ref(tex_coords)),
(
"TexCoordIndices",
Value::List(tex_coord_indices.iter().copied().map(Value::Ref).collect()),
),
];
Ok(tx.create(build_named(schema, ENTITY, values)?))
}
const TEXT_ALIGNMENT: &[&str] = &["left", "right", "center", "justify"];
const TEXT_DECORATION: &[&str] = &["none", "underline", "overline", "line-through", "blink"];
const TEXT_TRANSFORMATION: &[&str] = &["capitalize", "uppercase", "lowercase", "none"];
fn closed(
entity: &'static str,
attribute: &'static str,
value: Option<&str>,
permitted: &[&str],
) -> StyleResult<Value> {
let Some(text) = value else {
return Ok(Value::Null);
};
if !permitted.contains(&text) {
return Err(invalid_authoring(entity, attribute, text));
}
Ok(Value::Text(text.into()))
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SizeValue<'a> {
Descriptive(&'a str),
Length(f64),
PositiveLength(f64),
Ratio(f64),
NormalisedRatio(f64),
PositiveRatio(f64),
}
impl SizeValue<'_> {
fn into_value(self, entity: &'static str, attribute: &'static str) -> StyleResult<Value> {
let (type_name, inner) = match self {
Self::Descriptive(text) => {
if text.trim().is_empty() {
return Err(invalid_authoring(entity, attribute, "blank"));
}
("IFCDESCRIPTIVEMEASURE", Value::Text(text.into()))
}
Self::Length(v) => ("IFCLENGTHMEASURE", finite(entity, attribute, v)?),
Self::PositiveLength(v) => {
if v <= 0.0 {
return Err(invalid_authoring(entity, attribute, v));
}
("IFCPOSITIVELENGTHMEASURE", finite(entity, attribute, v)?)
}
Self::Ratio(v) => ("IFCRATIOMEASURE", finite(entity, attribute, v)?),
Self::NormalisedRatio(v) => {
if !(0.0..=1.0).contains(&v) {
return Err(invalid_authoring(entity, attribute, v));
}
("IFCNORMALISEDRATIOMEASURE", finite(entity, attribute, v)?)
}
Self::PositiveRatio(v) => {
if v <= 0.0 {
return Err(invalid_authoring(entity, attribute, v));
}
("IFCPOSITIVERATIOMEASURE", finite(entity, attribute, v)?)
}
};
Ok(Value::Typed {
type_name: type_name.into(),
value: Box::new(inner),
})
}
}
fn finite(entity: &'static str, attribute: &'static str, value: f64) -> StyleResult<Value> {
if !value.is_finite() {
return Err(invalid_authoring(entity, attribute, value));
}
Ok(Value::Real(value))
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct TextModelDraft<'a> {
pub text_indent: Option<SizeValue<'a>>,
pub text_align: Option<&'a str>,
pub text_decoration: Option<&'a str>,
pub letter_spacing: Option<SizeValue<'a>>,
pub word_spacing: Option<SizeValue<'a>>,
pub text_transform: Option<&'a str>,
pub line_height: Option<SizeValue<'a>>,
}
pub fn create_text_style_text_model(
tx: &mut Transaction,
schema: &Schema,
draft: TextModelDraft<'_>,
) -> StyleResult<EntityId> {
const ENTITY: &str = "IfcTextStyleTextModel";
let values = vec![
("TextIndent", size(ENTITY, "TextIndent", draft.text_indent)?),
(
"TextAlign",
closed(ENTITY, "TextAlign", draft.text_align, TEXT_ALIGNMENT)?,
),
(
"TextDecoration",
closed(
ENTITY,
"TextDecoration",
draft.text_decoration,
TEXT_DECORATION,
)?,
),
(
"LetterSpacing",
size(ENTITY, "LetterSpacing", draft.letter_spacing)?,
),
(
"WordSpacing",
size(ENTITY, "WordSpacing", draft.word_spacing)?,
),
(
"TextTransform",
closed(
ENTITY,
"TextTransform",
draft.text_transform,
TEXT_TRANSFORMATION,
)?,
),
("LineHeight", size(ENTITY, "LineHeight", draft.line_height)?),
];
Ok(tx.create(build_named(schema, ENTITY, values)?))
}
fn size(
entity: &'static str,
attribute: &'static str,
value: Option<SizeValue<'_>>,
) -> StyleResult<Value> {
value.map_or(Ok(Value::Null), |v| v.into_value(entity, attribute))
}
pub fn create_text_style(
tx: &mut Transaction,
schema: &Schema,
name: Option<&str>,
text_character_appearance: Option<EntityId>,
text_style: Option<EntityId>,
text_font_style: EntityId,
model_or_draughting: Option<bool>,
) -> StyleResult<EntityId> {
const ENTITY: &str = "IfcTextStyle";
if name.is_some_and(|text| text.trim().is_empty()) {
return Err(invalid_authoring(ENTITY, "Name", "blank"));
}
let mut values = vec![("TextFontStyle", Value::Ref(text_font_style))];
optional_text(&mut values, "Name", name);
optional_reference(
&mut values,
"TextCharacterAppearance",
text_character_appearance,
);
optional_reference(&mut values, "TextStyle", text_style);
if let Some(flag) = model_or_draughting {
values.push(("ModelOrDraughting", Value::Bool(flag)));
}
Ok(tx.create(build_named(schema, ENTITY, values)?))
}
pub fn create_indexed_triangle_texture_map(
tx: &mut Transaction,
schema: &Schema,
maps: &[EntityId],
mapped_to: EntityId,
tex_coords: EntityId,
tex_coord_index: &[[i64; 3]],
) -> StyleResult<EntityId> {
const ENTITY: &str = "IfcIndexedTriangleTextureMap";
if maps.is_empty() {
return Err(invalid_authoring(ENTITY, "Maps", "empty"));
}
for triangle in tex_coord_index {
if let Some(index) = triangle.iter().find(|value| **value < 1) {
return Err(invalid_authoring(
ENTITY,
"TexCoordIndex",
format!("{index} is not a positive one-based index"),
));
}
}
let mut values = vec![
(
"Maps",
Value::List(maps.iter().copied().map(Value::Ref).collect()),
),
("MappedTo", Value::Ref(mapped_to)),
("TexCoords", Value::Ref(tex_coords)),
];
if !tex_coord_index.is_empty() {
values.push((
"TexCoordIndex",
Value::List(
tex_coord_index
.iter()
.map(|triangle| {
Value::List(triangle.iter().copied().map(Value::Integer).collect())
})
.collect(),
),
));
}
Ok(tx.create(build_named(schema, ENTITY, values)?))
}
pub fn create_light_distribution_data(
tx: &mut Transaction,
schema: &Schema,
main_plane_angle: f64,
secondary_plane_angles: &[f64],
luminous_intensities: &[f64],
) -> StyleResult<EntityId> {
const ENTITY: &str = "IfcLightDistributionData";
if secondary_plane_angles.is_empty() || luminous_intensities.is_empty() {
return Err(invalid_authoring(ENTITY, "SecondaryPlaneAngle", "empty"));
}
if secondary_plane_angles.len() != luminous_intensities.len() {
return Err(invalid_authoring(
ENTITY,
"LuminousIntensity",
"expected one intensity per secondary angle",
));
}
if !main_plane_angle.is_finite()
|| secondary_plane_angles.iter().any(|v| !v.is_finite())
|| luminous_intensities.iter().any(|v| !v.is_finite())
{
return Err(invalid_authoring(ENTITY, "MainPlaneAngle", "not finite"));
}
let reals = |values: &[f64]| Value::List(values.iter().copied().map(Value::Real).collect());
let values = vec![
("MainPlaneAngle", Value::Real(main_plane_angle)),
("SecondaryPlaneAngle", reals(secondary_plane_angles)),
("LuminousIntensity", reals(luminous_intensities)),
];
Ok(tx.create(build_named(schema, ENTITY, values)?))
}
pub fn create_light_intensity_distribution(
tx: &mut Transaction,
schema: &Schema,
light_distribution_curve: &str,
distribution_data: &[EntityId],
) -> StyleResult<EntityId> {
const ENTITY: &str = "IfcLightIntensityDistribution";
if distribution_data.is_empty() {
return Err(invalid_authoring(ENTITY, "DistributionData", "empty"));
}
let declared = schema
.attributes(ENTITY)
.iter()
.find(|a| a.name.eq_ignore_ascii_case("LightDistributionCurve"))
.and_then(|a| schema.type_def(&a.type_name))
.is_some_and(|def| match &def.kind {
ifc_schema::TypeKind::Enumeration(values) => values
.iter()
.any(|v| v.eq_ignore_ascii_case(light_distribution_curve)),
_ => false,
});
if !declared {
return Err(invalid_authoring(
ENTITY,
"LightDistributionCurve",
light_distribution_curve,
));
}
let values = vec![
(
"LightDistributionCurve",
Value::Enum(light_distribution_curve.into()),
),
(
"DistributionData",
Value::List(distribution_data.iter().copied().map(Value::Ref).collect()),
),
];
Ok(tx.create(build_named(schema, ENTITY, values)?))
}
mod builders;