Skip to main content

ifc_style/authoring/
presentation.rs

1//! Curve, fill and text presentation styles plus texture coordinate data.
2
3use ifc_model::{EntityId, Transaction, Value};
4use ifc_schema::Schema;
5
6use crate::error::StyleResult;
7
8use super::{build_named, invalid_authoring, optional_text, schema_attribute};
9
10/// Stage an `IfcCurveStyleFontPattern`.
11///
12/// # Errors
13///
14/// Refuses a negative visible length or a non-positive invisible
15/// length (VisibleLengthGreaterEqualZero, and the positive measure
16/// type on InvisibleSegmentLength).
17pub fn create_curve_style_font_pattern(
18    tx: &mut Transaction,
19    schema: &Schema,
20    visible: f64,
21    invisible: f64,
22) -> StyleResult<EntityId> {
23    const ENTITY: &str = "IfcCurveStyleFontPattern";
24    if !visible.is_finite() || visible < 0.0 {
25        return Err(invalid_authoring(
26            ENTITY,
27            "VisibleSegmentLength",
28            format!("{visible}"),
29        ));
30    }
31    if !invisible.is_finite() || invisible <= 0.0 {
32        return Err(invalid_authoring(
33            ENTITY,
34            "InvisibleSegmentLength",
35            format!("{invisible}"),
36        ));
37    }
38    let values = vec![
39        ("VisibleSegmentLength", Value::Real(visible)),
40        ("InvisibleSegmentLength", Value::Real(invisible)),
41    ];
42    Ok(tx.create(build_named(schema, ENTITY, values)?))
43}
44
45/// Stage an `IfcCurveStyleFont`.
46///
47/// # Errors
48///
49/// Refuses an empty pattern list: `PatternList` is `LIST [1:?]`.
50pub fn create_curve_style_font(
51    tx: &mut Transaction,
52    schema: &Schema,
53    name: Option<&str>,
54    patterns: &[EntityId],
55) -> StyleResult<EntityId> {
56    const ENTITY: &str = "IfcCurveStyleFont";
57    if patterns.is_empty() {
58        return Err(invalid_authoring(ENTITY, "PatternList", "empty"));
59    }
60    let mut values = Vec::new();
61    optional_text(&mut values, "Name", name);
62    values.push((
63        "PatternList",
64        Value::List(patterns.iter().copied().map(Value::Ref).collect()),
65    ));
66    Ok(tx.create(build_named(schema, ENTITY, values)?))
67}
68
69/// Stage an `IfcCurveStyleFontAndScaling`.
70///
71/// # Errors
72///
73/// Refuses a non-positive scaling factor: `CurveFontScaling` is an
74/// `IfcPositiveRatioMeasure`.
75pub fn create_curve_style_font_and_scaling(
76    tx: &mut Transaction,
77    schema: &Schema,
78    name: Option<&str>,
79    font: EntityId,
80    scaling: f64,
81) -> StyleResult<EntityId> {
82    const ENTITY: &str = "IfcCurveStyleFontAndScaling";
83    if !scaling.is_finite() || scaling <= 0.0 {
84        return Err(invalid_authoring(
85            ENTITY,
86            "CurveFontScaling",
87            format!("{scaling}"),
88        ));
89    }
90    let mut values = Vec::new();
91    optional_text(&mut values, "Name", name);
92    // IFC4 names slot 1 `CurveFont`; IFC4X3 renamed it `CurveStyleFont`.
93    let font_attribute = schema_attribute(schema, ENTITY, ["CurveStyleFont", "CurveFont"]);
94    values.push((font_attribute, Value::Ref(font)));
95    values.push(("CurveFontScaling", Value::Real(scaling)));
96    Ok(tx.create(build_named(schema, ENTITY, values)?))
97}
98
99/// Stage an `IfcCurveStyle`.
100///
101/// `curve_width` is an `IfcSizeSelect`. The schema admits only a
102/// positive length, or the descriptive literal `by layer`; a bare
103/// number carries no measure and is refused rather than guessed at.
104///
105/// # Errors
106///
107/// Refuses a style with no font, width or colour (IdentifiableCurveStyle)
108/// and a width that is neither a positive length nor `by layer`
109/// (MeasureOfWidth).
110pub fn create_curve_style(
111    tx: &mut Transaction,
112    schema: &Schema,
113    draft: CurveStyleDraft<'_>,
114) -> StyleResult<EntityId> {
115    const ENTITY: &str = "IfcCurveStyle";
116    if draft.curve_font.is_none() && draft.curve_width.is_none() && draft.curve_colour.is_none() {
117        return Err(invalid_authoring(
118            ENTITY,
119            "CurveFont",
120            "a curve style must carry a font, a width or a colour",
121        ));
122    }
123    let mut values = Vec::new();
124    optional_text(&mut values, "Name", draft.name);
125    if let Some(font) = draft.curve_font {
126        values.push(("CurveFont", Value::Ref(font)));
127    }
128    if let Some(width) = draft.curve_width {
129        values.push(("CurveWidth", width.into_value(ENTITY)?));
130    }
131    if let Some(colour) = draft.curve_colour {
132        values.push(("CurveColour", Value::Ref(colour)));
133    }
134    if let Some(flag) = draft.model_or_draughting {
135        values.push(("ModelOrDraughting", Value::Bool(flag)));
136    }
137    Ok(tx.create(build_named(schema, ENTITY, values)?))
138}
139
140/// The two curve widths `MeasureOfWidth` admits.
141///
142/// Modelled as a closed enum rather than a bare `f64`: the rule is not
143/// "any number", and a caller that passes an unwrapped literal would
144/// produce a file the schema rejects.
145#[derive(Debug, Clone, Copy, PartialEq)]
146pub enum CurveWidth {
147    /// An `IfcPositiveLengthMeasure`.
148    PositiveLength(f64),
149    /// The descriptive measure `by layer`, deferring width to the layer.
150    ByLayer,
151}
152
153impl CurveWidth {
154    fn into_value(self, entity: &'static str) -> StyleResult<Value> {
155        match self {
156            Self::PositiveLength(length) if length.is_finite() && length > 0.0 => {
157                Ok(Value::Typed {
158                    type_name: "IFCPOSITIVELENGTHMEASURE".into(),
159                    value: Box::new(Value::Real(length)),
160                })
161            }
162            Self::PositiveLength(length) => {
163                Err(invalid_authoring(entity, "CurveWidth", format!("{length}")))
164            }
165            Self::ByLayer => Ok(Value::Typed {
166                type_name: "IFCDESCRIPTIVEMEASURE".into(),
167                value: Box::new(Value::Text("by layer".into())),
168            }),
169        }
170    }
171}
172
173/// Attributes of an `IfcCurveStyle`.
174#[derive(Debug, Clone, Copy, Default)]
175pub struct CurveStyleDraft<'a> {
176    /// `Name`.
177    pub name: Option<&'a str>,
178    /// `CurveFont`: an `IfcCurveStyleFont` or `IfcCurveStyleFontAndScaling`.
179    pub curve_font: Option<EntityId>,
180    /// `CurveWidth`.
181    pub curve_width: Option<CurveWidth>,
182    /// `CurveColour`: an `IfcColourRgb` or predefined colour.
183    pub curve_colour: Option<EntityId>,
184    /// `ModelOrDraughting`.
185    pub model_or_draughting: Option<bool>,
186}
187
188/// Which `IfcFillStyleSelect` member a fill style is.
189///
190/// The caller states the kind rather than the writer resolving it: a
191/// staged member cannot be read back out of a `Transaction`, so the
192/// counting rules could not otherwise be checked before commit.
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum FillStyleKind {
195    /// An `IfcColour`: at most one per fill area style.
196    Colour,
197    /// An `IfcExternallyDefinedHatchStyle`: at most one.
198    ExternalHatchStyle,
199    /// An `IfcFillAreaStyleHatching`.
200    Hatching,
201    /// An `IfcFillAreaStyleTiles`.
202    Tiles,
203}
204
205/// Stage an `IfcFillAreaStyle`.
206///
207/// # Errors
208///
209/// Refuses an empty style set, more than one colour (MaxOneColour), and
210/// more than one externally defined hatch style (MaxOneExtHatchStyle).
211pub fn create_fill_area_style(
212    tx: &mut Transaction,
213    schema: &Schema,
214    name: Option<&str>,
215    styles: &[(EntityId, FillStyleKind)],
216    model_or_draughting: Option<bool>,
217) -> StyleResult<EntityId> {
218    const ENTITY: &str = "IfcFillAreaStyle";
219    if styles.is_empty() {
220        return Err(invalid_authoring(ENTITY, "FillStyles", "empty"));
221    }
222    for (kind, attribute) in [
223        (FillStyleKind::Colour, "MaxOneColour"),
224        (FillStyleKind::ExternalHatchStyle, "MaxOneExtHatchStyle"),
225    ] {
226        let found = styles.iter().filter(|(_, k)| *k == kind).count();
227        if found > 1 {
228            return Err(invalid_authoring(ENTITY, attribute, format!("{found}")));
229        }
230    }
231    let mut values = Vec::new();
232    optional_text(&mut values, "Name", name);
233    values.push((
234        "FillStyles",
235        Value::List(styles.iter().map(|(id, _)| Value::Ref(*id)).collect()),
236    ));
237    if let Some(flag) = model_or_draughting {
238        // IFC4 spells this `ModelorDraughting`; IFC4X3 capitalised the O.
239        let flag_attribute = "ModelOrDraughting";
240        values.push((flag_attribute, Value::Bool(flag)));
241    }
242    Ok(tx.create(build_named(schema, ENTITY, values)?))
243}
244
245/// Stage an `IfcTextStyleForDefinedFont`.
246pub fn create_text_style_for_defined_font(
247    tx: &mut Transaction,
248    schema: &Schema,
249    colour: EntityId,
250    background: Option<EntityId>,
251) -> StyleResult<EntityId> {
252    let mut values = vec![("Colour", Value::Ref(colour))];
253    if let Some(background) = background {
254        values.push(("BackgroundColour", Value::Ref(background)));
255    }
256    Ok(tx.create(build_named(schema, "IfcTextStyleForDefinedFont", values)?))
257}
258
259/// Stage an `IfcTextStyleFontModel`.
260///
261/// # Errors
262///
263/// Refuses a blank name, an empty font family, and a non-positive font
264/// size. `MeasureOfFontSize` also requires the size to be an
265/// `IfcLengthMeasure`, so it is written with that wrapper.
266pub fn create_text_style_font_model(
267    tx: &mut Transaction,
268    schema: &Schema,
269    name: &str,
270    font_family: &[&str],
271    font_size: f64,
272) -> StyleResult<EntityId> {
273    const ENTITY: &str = "IfcTextStyleFontModel";
274    if name.trim().is_empty() {
275        return Err(invalid_authoring(ENTITY, "Name", name));
276    }
277    if font_family.is_empty() {
278        return Err(invalid_authoring(ENTITY, "FontFamily", "empty"));
279    }
280    if !font_size.is_finite() || font_size <= 0.0 {
281        return Err(invalid_authoring(
282            ENTITY,
283            "FontSize",
284            format!("{font_size}"),
285        ));
286    }
287    let values = vec![
288        ("Name", Value::Text(name.into())),
289        (
290            "FontFamily",
291            Value::List(
292                font_family
293                    .iter()
294                    .map(|family| Value::Text((*family).into()))
295                    .collect(),
296            ),
297        ),
298        (
299            "FontSize",
300            Value::Typed {
301                type_name: "IFCLENGTHMEASURE".into(),
302                value: Box::new(Value::Real(font_size)),
303            },
304        ),
305    ];
306    Ok(tx.create(build_named(schema, ENTITY, values)?))
307}
308
309/// The names `PreDefinedColourNames` admits, lower-case as the schema states.
310pub const PREDEFINED_COLOUR_NAMES: &[&str] = &[
311    "black", "red", "green", "blue", "yellow", "magenta", "cyan", "white", "by layer",
312];
313
314/// The names `PreDefinedCurveFontNames` admits.
315pub const PREDEFINED_CURVE_FONT_NAMES: &[&str] = &[
316    "continuous",
317    "chain",
318    "chain double dash",
319    "dashed",
320    "dotted",
321    "by layer",
322];
323
324/// Stage an `IfcDraughtingPreDefinedColour`.
325///
326/// # Errors
327///
328/// Refuses a name outside `PREDEFINED_COLOUR_NAMES`. The comparison is
329/// case-sensitive: the schema states the members in lower case, and a
330/// reader matching them literally would miss `Black`.
331pub fn create_draughting_predefined_colour(
332    tx: &mut Transaction,
333    schema: &Schema,
334    name: &str,
335) -> StyleResult<EntityId> {
336    const ENTITY: &str = "IfcDraughtingPreDefinedColour";
337    if !PREDEFINED_COLOUR_NAMES.contains(&name) {
338        return Err(invalid_authoring(ENTITY, "Name", name));
339    }
340    let values = vec![("Name", Value::Text(name.into()))];
341    Ok(tx.create(build_named(schema, ENTITY, values)?))
342}
343
344/// Stage an `IfcDraughtingPreDefinedCurveFont`.
345///
346/// # Errors
347///
348/// Refuses a name outside `PREDEFINED_CURVE_FONT_NAMES`.
349pub fn create_draughting_predefined_curve_font(
350    tx: &mut Transaction,
351    schema: &Schema,
352    name: &str,
353) -> StyleResult<EntityId> {
354    const ENTITY: &str = "IfcDraughtingPreDefinedCurveFont";
355    if !PREDEFINED_CURVE_FONT_NAMES.contains(&name) {
356        return Err(invalid_authoring(ENTITY, "Name", name));
357    }
358    let values = vec![("Name", Value::Text(name.into()))];
359    Ok(tx.create(build_named(schema, ENTITY, values)?))
360}
361
362/// Stage an `IfcPixelTexture`.
363///
364/// `pixel` holds one binary literal per pixel, hex-encoded without the
365/// STEP quotes. All five WHERE rules are checked here.
366///
367/// # Errors
368///
369/// Refuses a width or height below one (MinPixelInS, MinPixelInT), a
370/// colour-component count outside 1..=4 (NumberOfColours), a pixel count
371/// that is not `width * height` (SizeOfPixelList), and pixels that are
372/// not whole bytes or differ in length from the first (
373/// PixelAsByteAndSameLength).
374pub fn create_pixel_texture(
375    tx: &mut Transaction,
376    schema: &Schema,
377    draft: PixelTextureDraft<'_>,
378) -> StyleResult<EntityId> {
379    const ENTITY: &str = "IfcPixelTexture";
380    if draft.width < 1 {
381        return Err(invalid_authoring(
382            ENTITY,
383            "Width",
384            format!("{}", draft.width),
385        ));
386    }
387    if draft.height < 1 {
388        return Err(invalid_authoring(
389            ENTITY,
390            "Height",
391            format!("{}", draft.height),
392        ));
393    }
394    if !(1..=4).contains(&draft.colour_components) {
395        return Err(invalid_authoring(
396            ENTITY,
397            "ColourComponents",
398            format!("{}", draft.colour_components),
399        ));
400    }
401    let expected = i64::from(draft.width) * i64::from(draft.height);
402    if draft.pixel.len() as i64 != expected {
403        return Err(invalid_authoring(
404            ENTITY,
405            "SizeOfPixelList",
406            format!("{} pixels for {expected} cells", draft.pixel.len()),
407        ));
408    }
409    // BLENGTH counts bits: a hex literal carries four bits per digit, so a
410    // whole number of bytes means an even digit count.
411    let first = draft.pixel[0].len();
412    for pixel in draft.pixel {
413        if pixel.len() % 2 != 0 || pixel.len() != first {
414            return Err(invalid_authoring(
415                ENTITY,
416                "PixelAsByteAndSameLength",
417                pixel.to_string(),
418            ));
419        }
420    }
421    let mut values = vec![
422        ("RepeatS", Value::Bool(draft.repeat_s)),
423        ("RepeatT", Value::Bool(draft.repeat_t)),
424    ];
425    optional_text(&mut values, "Mode", draft.mode);
426    values.extend([
427        ("Width", Value::Integer(i64::from(draft.width))),
428        ("Height", Value::Integer(i64::from(draft.height))),
429        (
430            "ColourComponents",
431            Value::Integer(i64::from(draft.colour_components)),
432        ),
433        (
434            "Pixel",
435            Value::List(
436                draft
437                    .pixel
438                    .iter()
439                    .map(|pixel| Value::Binary((*pixel).into()))
440                    .collect(),
441            ),
442        ),
443    ]);
444    Ok(tx.create(build_named(schema, ENTITY, values)?))
445}
446
447/// Attributes of an `IfcPixelTexture`.
448#[derive(Debug, Clone, Copy)]
449pub struct PixelTextureDraft<'a> {
450    /// `RepeatS`.
451    pub repeat_s: bool,
452    /// `RepeatT`.
453    pub repeat_t: bool,
454    /// `Mode`.
455    pub mode: Option<&'a str>,
456    /// `Width`, at least one.
457    pub width: i32,
458    /// `Height`, at least one.
459    pub height: i32,
460    /// `ColourComponents`, in `1..=4`.
461    pub colour_components: i32,
462    /// `Pixel`: hex literals, one per cell, all the same length.
463    pub pixel: &'a [&'a str],
464}
465
466/// Stage an `IfcTextureVertex`: one 2-tuple of parameter values.
467///
468/// # Errors
469///
470/// Refuses a non-finite coordinate. The pair arity is carried by the
471/// argument type, so a wrong-length vertex cannot be expressed.
472pub fn create_texture_vertex(
473    tx: &mut Transaction,
474    schema: &Schema,
475    coordinates: [f64; 2],
476) -> StyleResult<EntityId> {
477    const ENTITY: &str = "IfcTextureVertex";
478    for value in coordinates {
479        if !value.is_finite() {
480            return Err(invalid_authoring(ENTITY, "Coordinates", format!("{value}")));
481        }
482    }
483    let values = vec![(
484        "Coordinates",
485        Value::List(coordinates.into_iter().map(Value::Real).collect()),
486    )];
487    Ok(tx.create(build_named(schema, ENTITY, values)?))
488}
489
490/// Stage an `IfcTextureVertexList`.
491///
492/// # Errors
493///
494/// Refuses an empty list and any non-finite coordinate.
495pub fn create_texture_vertex_list(
496    tx: &mut Transaction,
497    schema: &Schema,
498    coordinates: &[[f64; 2]],
499) -> StyleResult<EntityId> {
500    const ENTITY: &str = "IfcTextureVertexList";
501    if coordinates.is_empty() {
502        return Err(invalid_authoring(ENTITY, "TexCoordsList", "empty"));
503    }
504    for pair in coordinates {
505        for value in pair {
506            if !value.is_finite() {
507                return Err(invalid_authoring(
508                    ENTITY,
509                    "TexCoordsList",
510                    format!("{value}"),
511                ));
512            }
513        }
514    }
515    let values = vec![(
516        "TexCoordsList",
517        Value::List(
518            coordinates
519                .iter()
520                .map(|pair| Value::List(pair.iter().copied().map(Value::Real).collect()))
521                .collect(),
522        ),
523    )];
524    Ok(tx.create(build_named(schema, ENTITY, values)?))
525}
526
527/// Stage an `IfcColourRgbList`.
528///
529/// # Errors
530///
531/// Refuses an empty list and any channel outside `[0, 1]`: the inner
532/// members are `IfcNormalisedRatioMeasure`, not free reals.
533pub fn create_colour_rgb_list(
534    tx: &mut Transaction,
535    schema: &Schema,
536    colours: &[[f64; 3]],
537) -> StyleResult<EntityId> {
538    const ENTITY: &str = "IfcColourRgbList";
539    if colours.is_empty() {
540        return Err(invalid_authoring(ENTITY, "ColourList", "empty"));
541    }
542    for colour in colours {
543        for channel in colour {
544            if !channel.is_finite() || *channel < 0.0 || *channel > 1.0 {
545                return Err(invalid_authoring(
546                    ENTITY,
547                    "ColourList",
548                    format!("{channel}"),
549                ));
550            }
551        }
552    }
553    let values = vec![(
554        "ColourList",
555        Value::List(
556            colours
557                .iter()
558                .map(|colour| Value::List(colour.iter().copied().map(Value::Real).collect()))
559                .collect(),
560        ),
561    )];
562    Ok(tx.create(build_named(schema, ENTITY, values)?))
563}
564
565/// Stage an `IfcIndexedColourMap`.
566///
567/// # Errors
568///
569/// Refuses an empty colour index, a non-positive index (the members are
570/// `IfcPositiveInteger`, and STEP indices are one-based), and an opacity
571/// outside `[0, 1]`.
572pub fn create_indexed_colour_map(
573    tx: &mut Transaction,
574    schema: &Schema,
575    mapped_to: EntityId,
576    colours: EntityId,
577    colour_index: &[i64],
578    opacity: Option<f64>,
579) -> StyleResult<EntityId> {
580    const ENTITY: &str = "IfcIndexedColourMap";
581    if colour_index.is_empty() {
582        return Err(invalid_authoring(ENTITY, "ColourIndex", "empty"));
583    }
584    if let Some(index) = colour_index.iter().find(|index| **index < 1) {
585        return Err(invalid_authoring(ENTITY, "ColourIndex", format!("{index}")));
586    }
587    if let Some(value) = opacity {
588        if !value.is_finite() || !(0.0..=1.0).contains(&value) {
589            return Err(invalid_authoring(ENTITY, "Opacity", format!("{value}")));
590        }
591    }
592    let mut values = vec![("MappedTo", Value::Ref(mapped_to))];
593    if let Some(value) = opacity {
594        values.push(("Opacity", Value::Real(value)));
595    }
596    values.extend([
597        ("Colours", Value::Ref(colours)),
598        (
599            "ColourIndex",
600            Value::List(colour_index.iter().copied().map(Value::Integer).collect()),
601        ),
602    ]);
603    Ok(tx.create(build_named(schema, ENTITY, values)?))
604}
605
606/// Stage an `IfcPresentationLayerAssignment`.
607///
608/// # Errors
609///
610/// Refuses a blank name and an empty item set (ApplicableItems).
611pub fn create_presentation_layer_assignment(
612    tx: &mut Transaction,
613    schema: &Schema,
614    name: &str,
615    description: Option<&str>,
616    assigned_items: &[EntityId],
617    identifier: Option<&str>,
618) -> StyleResult<EntityId> {
619    const ENTITY: &str = "IfcPresentationLayerAssignment";
620    if name.trim().is_empty() {
621        return Err(invalid_authoring(ENTITY, "Name", name));
622    }
623    if assigned_items.is_empty() {
624        return Err(invalid_authoring(ENTITY, "AssignedItems", "empty"));
625    }
626    let mut values = vec![("Name", Value::Text(name.into()))];
627    optional_text(&mut values, "Description", description);
628    values.push((
629        "AssignedItems",
630        Value::List(assigned_items.iter().copied().map(Value::Ref).collect()),
631    ));
632    optional_text(&mut values, "Identifier", identifier);
633    Ok(tx.create(build_named(schema, ENTITY, values)?))
634}