Skip to main content

ifc_style/authoring/
texture.rs

1//! Textures: the image source, its coordinates, and generated maps.
2//!
3//! # Why a texture needs a face to mean anything
4//!
5//! `IfcTextureMap` carries `Vertices : LIST [3:?] OF IfcTextureVertex`
6//! and `MappedTo : IfcFace`. The vertex list is a correspondence: the
7//! nth texture vertex belongs to the nth vertex of the face's bound.
8//! A list shorter than the face's loop leaves vertices unmapped, and a
9//! longer one carries coordinates for vertices that do not exist.
10//!
11//! The writer cannot check that correspondence without walking the
12//! face's loop, which is geometry this crate deliberately does not
13//! depend on -- `ifc-style` may reference a representation item's id
14//! but never inspects its shape. So the cardinality floor of three is
15//! enforced here and the correspondence is left to the consumer that
16//! owns the geometry. Claiming otherwise would be a writer asserting
17//! something it cannot see.
18
19use ifc_model::{EntityId, Model, Transaction, Value};
20use ifc_schema::Schema;
21
22use crate::authoring::{build_named, invalid_authoring, validate_ref};
23use crate::StyleResult;
24
25/// What to stage for an `IfcImageTexture`.
26#[derive(Debug, Clone, Copy)]
27pub struct ImageTextureDraft<'a> {
28    /// `RepeatS`: does the texture tile along the s axis?
29    pub repeat_s: bool,
30    /// `RepeatT`: does the texture tile along the t axis?
31    pub repeat_t: bool,
32    /// `Mode`: how the texture combines with the underlying colour.
33    pub mode: Option<&'a str>,
34    /// `TextureTransform`: an `IfcCartesianTransformationOperator2D`.
35    pub texture_transform: Option<EntityId>,
36    /// `URLReference`: where the image lives.
37    pub url_reference: &'a str,
38}
39
40/// Stage an `IfcImageTexture`.
41///
42/// # The URL is required and must not be blank
43///
44/// `URLReference` is the only attribute that says what the texture
45/// actually is. An empty string satisfies the schema's type but names
46/// no image, producing a texture that resolves to nothing -- so it is
47/// refused rather than written.
48///
49/// # Errors
50///
51/// Refuses a blank `URLReference`, and a `TextureTransform` that does
52/// not resolve to an `IfcCartesianTransformationOperator2D`.
53pub fn create_image_texture(
54    tx: &mut Transaction,
55    model: &Model,
56    schema: &Schema,
57    draft: ImageTextureDraft<'_>,
58) -> StyleResult<EntityId> {
59    if draft.url_reference.trim().is_empty() {
60        return Err(invalid_authoring(
61            "IfcImageTexture",
62            "URLReference",
63            "expected a reference to an image, found nothing",
64        ));
65    }
66
67    if let Some(transform) = draft.texture_transform {
68        validate_ref(
69            tx,
70            model,
71            schema,
72            transform,
73            "IfcCartesianTransformationOperator2D",
74        )?;
75    }
76
77    let mut values = vec![
78        ("RepeatS", Value::Bool(draft.repeat_s)),
79        ("RepeatT", Value::Bool(draft.repeat_t)),
80        ("URLReference", Value::Text(draft.url_reference.into())),
81    ];
82    if let Some(mode) = draft.mode {
83        values.push(("Mode", Value::Text(mode.into())));
84    }
85    if let Some(transform) = draft.texture_transform {
86        values.push(("TextureTransform", Value::Ref(transform)));
87    }
88
89    Ok(tx.create(build_named(schema, "IfcImageTexture", values)?))
90}
91
92/// Stage an `IfcTextureMap`: explicit texture coordinates on a face.
93///
94/// # Errors
95///
96/// Refuses fewer than three vertices, since a mapped area needs at
97/// least a triangle; an empty `maps` list, since the record exists to
98/// carry those textures; and a `mapped_to` that does not resolve to an
99/// `IfcFace`.
100pub fn create_texture_map(
101    tx: &mut Transaction,
102    model: &Model,
103    schema: &Schema,
104    maps: &[EntityId],
105    vertices: &[EntityId],
106    mapped_to: EntityId,
107) -> StyleResult<EntityId> {
108    if maps.is_empty() {
109        return Err(invalid_authoring(
110            "IfcTextureMap",
111            "Maps",
112            "expected at least one surface texture, found none",
113        ));
114    }
115    if vertices.len() < 3 {
116        return Err(invalid_authoring(
117            "IfcTextureMap",
118            "Vertices",
119            format!("expected at least 3 vertices, found {}", vertices.len()),
120        ));
121    }
122
123    validate_ref(tx, model, schema, mapped_to, "IfcFace")?;
124
125    let values = vec![
126        (
127            "Maps",
128            Value::List(maps.iter().copied().map(Value::Ref).collect()),
129        ),
130        (
131            "Vertices",
132            Value::List(vertices.iter().copied().map(Value::Ref).collect()),
133        ),
134        ("MappedTo", Value::Ref(mapped_to)),
135    ];
136
137    Ok(tx.create(build_named(schema, "IfcTextureMap", values)?))
138}
139
140/// Stage an `IfcTextureCoordinateGenerator`: coordinates computed, not listed.
141///
142/// `mode` names the generating function (`SPHERE`, `CYLINDER`, and so
143/// on). Unlike [`create_texture_map`] there are no vertices: the
144/// consumer derives them from the mode and parameters.
145///
146/// # Errors
147///
148/// Refuses an empty `maps` list, a blank `mode` -- which would leave
149/// the generator with no function to apply -- and a non-finite
150/// parameter.
151pub fn create_texture_coordinate_generator(
152    tx: &mut Transaction,
153    schema: &Schema,
154    maps: &[EntityId],
155    mode: &str,
156    parameters: &[f64],
157) -> StyleResult<EntityId> {
158    if maps.is_empty() {
159        return Err(invalid_authoring(
160            "IfcTextureCoordinateGenerator",
161            "Maps",
162            "expected at least one surface texture, found none",
163        ));
164    }
165    if mode.trim().is_empty() {
166        return Err(invalid_authoring(
167            "IfcTextureCoordinateGenerator",
168            "Mode",
169            "expected a generating function, found nothing",
170        ));
171    }
172    if let Some(value) = parameters.iter().find(|value| !value.is_finite()) {
173        return Err(invalid_authoring(
174            "IfcTextureCoordinateGenerator",
175            "Parameter",
176            format!("expected finite parameters, found {value}"),
177        ));
178    }
179
180    let mut values = vec![
181        (
182            "Maps",
183            Value::List(maps.iter().copied().map(Value::Ref).collect()),
184        ),
185        ("Mode", Value::Text(mode.into())),
186    ];
187    if !parameters.is_empty() {
188        values.push((
189            "Parameter",
190            Value::List(parameters.iter().copied().map(Value::Real).collect()),
191        ));
192    }
193
194    Ok(tx.create(build_named(
195        schema,
196        "IfcTextureCoordinateGenerator",
197        values,
198    )?))
199}