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)]
175#[non_exhaustive]
176pub struct CurveStyleDraft<'a> {
177    /// `Name`.
178    pub name: Option<&'a str>,
179    /// `CurveFont`: an `IfcCurveStyleFont` or `IfcCurveStyleFontAndScaling`.
180    pub curve_font: Option<EntityId>,
181    /// `CurveWidth`.
182    pub curve_width: Option<CurveWidth>,
183    /// `CurveColour`: an `IfcColourRgb` or predefined colour.
184    pub curve_colour: Option<EntityId>,
185    /// `ModelOrDraughting`.
186    pub model_or_draughting: Option<bool>,
187}
188
189impl<'a> CurveStyleDraft<'a> {
190    /// Starts a draft with every field unset.
191    #[must_use]
192    pub fn new() -> Self {
193        Self::default()
194    }
195
196    /// Sets `name`.
197    ///
198    /// `Name`.
199    #[must_use]
200    pub fn name(mut self, value: &'a str) -> Self {
201        self.name = Some(value);
202        self
203    }
204
205    /// Sets `curve_font`.
206    ///
207    /// `CurveFont`: an `IfcCurveStyleFont` or `IfcCurveStyleFontAndScaling`.
208    #[must_use]
209    pub fn curve_font(mut self, value: EntityId) -> Self {
210        self.curve_font = Some(value);
211        self
212    }
213
214    /// Sets `curve_width`.
215    ///
216    /// `CurveWidth`.
217    #[must_use]
218    pub fn curve_width(mut self, value: CurveWidth) -> Self {
219        self.curve_width = Some(value);
220        self
221    }
222
223    /// Sets `curve_colour`.
224    ///
225    /// `CurveColour`: an `IfcColourRgb` or predefined colour.
226    #[must_use]
227    pub fn curve_colour(mut self, value: EntityId) -> Self {
228        self.curve_colour = Some(value);
229        self
230    }
231
232    /// Sets `model_or_draughting`.
233    ///
234    /// `ModelOrDraughting`.
235    #[must_use]
236    pub fn model_or_draughting(mut self, value: bool) -> Self {
237        self.model_or_draughting = Some(value);
238        self
239    }
240}
241
242/// Which `IfcFillStyleSelect` member a fill style is.
243///
244/// The caller states the kind rather than the writer resolving it: a
245/// staged member cannot be read back out of a `Transaction`, so the
246/// counting rules could not otherwise be checked before commit.
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248pub enum FillStyleKind {
249    /// An `IfcColour`: at most one per fill area style.
250    Colour,
251    /// An `IfcExternallyDefinedHatchStyle`: at most one.
252    ExternalHatchStyle,
253    /// An `IfcFillAreaStyleHatching`.
254    Hatching,
255    /// An `IfcFillAreaStyleTiles`.
256    Tiles,
257}
258
259/// Stage an `IfcFillAreaStyle`.
260///
261/// # Errors
262///
263/// Refuses an empty style set, more than one colour (MaxOneColour), and
264/// more than one externally defined hatch style (MaxOneExtHatchStyle).
265pub fn create_fill_area_style(
266    tx: &mut Transaction,
267    schema: &Schema,
268    name: Option<&str>,
269    styles: &[(EntityId, FillStyleKind)],
270    model_or_draughting: Option<bool>,
271) -> StyleResult<EntityId> {
272    const ENTITY: &str = "IfcFillAreaStyle";
273    if styles.is_empty() {
274        return Err(invalid_authoring(ENTITY, "FillStyles", "empty"));
275    }
276    for (kind, attribute) in [
277        (FillStyleKind::Colour, "MaxOneColour"),
278        (FillStyleKind::ExternalHatchStyle, "MaxOneExtHatchStyle"),
279    ] {
280        let found = styles.iter().filter(|(_, k)| *k == kind).count();
281        if found > 1 {
282            return Err(invalid_authoring(ENTITY, attribute, format!("{found}")));
283        }
284    }
285    let mut values = Vec::new();
286    optional_text(&mut values, "Name", name);
287    values.push((
288        "FillStyles",
289        Value::List(styles.iter().map(|(id, _)| Value::Ref(*id)).collect()),
290    ));
291    if let Some(flag) = model_or_draughting {
292        // IFC4 spells this `ModelorDraughting`; IFC4X3 capitalised the O.
293        let flag_attribute = "ModelOrDraughting";
294        values.push((flag_attribute, Value::Bool(flag)));
295    }
296    Ok(tx.create(build_named(schema, ENTITY, values)?))
297}
298
299/// Stage an `IfcTextStyleForDefinedFont`.
300pub fn create_text_style_for_defined_font(
301    tx: &mut Transaction,
302    schema: &Schema,
303    colour: EntityId,
304    background: Option<EntityId>,
305) -> StyleResult<EntityId> {
306    let mut values = vec![("Colour", Value::Ref(colour))];
307    if let Some(background) = background {
308        values.push(("BackgroundColour", Value::Ref(background)));
309    }
310    Ok(tx.create(build_named(schema, "IfcTextStyleForDefinedFont", values)?))
311}
312
313/// Stage an `IfcTextStyleFontModel`.
314///
315/// # Errors
316///
317/// Refuses a blank name, an empty font family, and a non-positive font
318/// size. `MeasureOfFontSize` also requires the size to be an
319/// `IfcLengthMeasure`, so it is written with that wrapper.
320pub fn create_text_style_font_model(
321    tx: &mut Transaction,
322    schema: &Schema,
323    name: &str,
324    font_family: &[&str],
325    font_size: f64,
326) -> StyleResult<EntityId> {
327    const ENTITY: &str = "IfcTextStyleFontModel";
328    if name.trim().is_empty() {
329        return Err(invalid_authoring(ENTITY, "Name", name));
330    }
331    if font_family.is_empty() {
332        return Err(invalid_authoring(ENTITY, "FontFamily", "empty"));
333    }
334    if !font_size.is_finite() || font_size <= 0.0 {
335        return Err(invalid_authoring(
336            ENTITY,
337            "FontSize",
338            format!("{font_size}"),
339        ));
340    }
341    let values = vec![
342        ("Name", Value::Text(name.into())),
343        (
344            "FontFamily",
345            Value::List(
346                font_family
347                    .iter()
348                    .map(|family| Value::Text((*family).into()))
349                    .collect(),
350            ),
351        ),
352        (
353            "FontSize",
354            Value::Typed {
355                type_name: "IFCLENGTHMEASURE".into(),
356                value: Box::new(Value::Real(font_size)),
357            },
358        ),
359    ];
360    Ok(tx.create(build_named(schema, ENTITY, values)?))
361}
362
363/// The names `PreDefinedColourNames` admits, lower-case as the schema states.
364pub const PREDEFINED_COLOUR_NAMES: &[&str] = &[
365    "black", "red", "green", "blue", "yellow", "magenta", "cyan", "white", "by layer",
366];
367
368/// The names `PreDefinedCurveFontNames` admits.
369pub const PREDEFINED_CURVE_FONT_NAMES: &[&str] = &[
370    "continuous",
371    "chain",
372    "chain double dash",
373    "dashed",
374    "dotted",
375    "by layer",
376];
377
378/// Stage an `IfcDraughtingPreDefinedColour`.
379///
380/// # Errors
381///
382/// Refuses a name outside `PREDEFINED_COLOUR_NAMES`. The comparison is
383/// case-sensitive: the schema states the members in lower case, and a
384/// reader matching them literally would miss `Black`.
385pub fn create_draughting_predefined_colour(
386    tx: &mut Transaction,
387    schema: &Schema,
388    name: &str,
389) -> StyleResult<EntityId> {
390    const ENTITY: &str = "IfcDraughtingPreDefinedColour";
391    if !PREDEFINED_COLOUR_NAMES.contains(&name) {
392        return Err(invalid_authoring(ENTITY, "Name", name));
393    }
394    let values = vec![("Name", Value::Text(name.into()))];
395    Ok(tx.create(build_named(schema, ENTITY, values)?))
396}
397
398/// Stage an `IfcDraughtingPreDefinedCurveFont`.
399///
400/// # Errors
401///
402/// Refuses a name outside `PREDEFINED_CURVE_FONT_NAMES`.
403pub fn create_draughting_predefined_curve_font(
404    tx: &mut Transaction,
405    schema: &Schema,
406    name: &str,
407) -> StyleResult<EntityId> {
408    const ENTITY: &str = "IfcDraughtingPreDefinedCurveFont";
409    if !PREDEFINED_CURVE_FONT_NAMES.contains(&name) {
410        return Err(invalid_authoring(ENTITY, "Name", name));
411    }
412    let values = vec![("Name", Value::Text(name.into()))];
413    Ok(tx.create(build_named(schema, ENTITY, values)?))
414}
415
416/// Stage an `IfcPixelTexture`.
417///
418/// `pixel` holds one binary literal per pixel, hex-encoded without the
419/// STEP quotes. All five WHERE rules are checked here.
420///
421/// # Errors
422///
423/// Refuses a width or height below one (MinPixelInS, MinPixelInT), a
424/// colour-component count outside 1..=4 (NumberOfColours), a pixel count
425/// that is not `width * height` (SizeOfPixelList), and pixels that are
426/// not whole bytes or differ in length from the first (
427/// PixelAsByteAndSameLength).
428pub fn create_pixel_texture(
429    tx: &mut Transaction,
430    schema: &Schema,
431    draft: PixelTextureDraft<'_>,
432) -> StyleResult<EntityId> {
433    const ENTITY: &str = "IfcPixelTexture";
434    if draft.width < 1 {
435        return Err(invalid_authoring(
436            ENTITY,
437            "Width",
438            format!("{}", draft.width),
439        ));
440    }
441    if draft.height < 1 {
442        return Err(invalid_authoring(
443            ENTITY,
444            "Height",
445            format!("{}", draft.height),
446        ));
447    }
448    if !(1..=4).contains(&draft.colour_components) {
449        return Err(invalid_authoring(
450            ENTITY,
451            "ColourComponents",
452            format!("{}", draft.colour_components),
453        ));
454    }
455    let expected = i64::from(draft.width) * i64::from(draft.height);
456    if draft.pixel.len() as i64 != expected {
457        return Err(invalid_authoring(
458            ENTITY,
459            "SizeOfPixelList",
460            format!("{} pixels for {expected} cells", draft.pixel.len()),
461        ));
462    }
463    // BLENGTH counts bits: a hex literal carries four bits per digit, so a
464    // whole number of bytes means an even digit count.
465    let first = draft.pixel[0].len();
466    for pixel in draft.pixel {
467        if pixel.len() % 2 != 0 || pixel.len() != first {
468            return Err(invalid_authoring(
469                ENTITY,
470                "PixelAsByteAndSameLength",
471                pixel.to_string(),
472            ));
473        }
474    }
475    let mut values = vec![
476        ("RepeatS", Value::Bool(draft.repeat_s)),
477        ("RepeatT", Value::Bool(draft.repeat_t)),
478    ];
479    optional_text(&mut values, "Mode", draft.mode);
480    values.extend([
481        ("Width", Value::Integer(i64::from(draft.width))),
482        ("Height", Value::Integer(i64::from(draft.height))),
483        (
484            "ColourComponents",
485            Value::Integer(i64::from(draft.colour_components)),
486        ),
487        (
488            "Pixel",
489            Value::List(
490                draft
491                    .pixel
492                    .iter()
493                    .map(|pixel| Value::Binary((*pixel).into()))
494                    .collect(),
495            ),
496        ),
497    ]);
498    Ok(tx.create(build_named(schema, ENTITY, values)?))
499}
500
501/// Attributes of an `IfcPixelTexture`.
502#[derive(Debug, Clone, Copy)]
503#[non_exhaustive]
504pub struct PixelTextureDraft<'a> {
505    /// `RepeatS`.
506    pub repeat_s: bool,
507    /// `RepeatT`.
508    pub repeat_t: bool,
509    /// `Mode`.
510    pub mode: Option<&'a str>,
511    /// `Width`, at least one.
512    pub width: i32,
513    /// `Height`, at least one.
514    pub height: i32,
515    /// `ColourComponents`, in `1..=4`.
516    pub colour_components: i32,
517    /// `Pixel`: hex literals, one per cell, all the same length.
518    pub pixel: &'a [&'a str],
519}
520
521impl<'a> PixelTextureDraft<'a> {
522    /// Starts a draft from its required `width`, `height`, `colour_components`,
523    /// `pixel`; every other field is unset.
524    #[must_use]
525    pub fn new(width: i32, height: i32, colour_components: i32, pixel: &'a [&'a str]) -> Self {
526        Self {
527            repeat_s: false,
528            repeat_t: false,
529            mode: None,
530            width,
531            height,
532            colour_components,
533            pixel,
534        }
535    }
536
537    /// Sets `repeat_s`.
538    ///
539    /// `RepeatS`.
540    #[must_use]
541    pub fn repeat_s(mut self, value: bool) -> Self {
542        self.repeat_s = value;
543        self
544    }
545
546    /// Sets `repeat_t`.
547    ///
548    /// `RepeatT`.
549    #[must_use]
550    pub fn repeat_t(mut self, value: bool) -> Self {
551        self.repeat_t = value;
552        self
553    }
554
555    /// Sets `mode`.
556    ///
557    /// `Mode`.
558    #[must_use]
559    pub fn mode(mut self, value: &'a str) -> Self {
560        self.mode = Some(value);
561        self
562    }
563}
564
565/// Stage an `IfcTextureVertex`: one 2-tuple of parameter values.
566///
567/// # Errors
568///
569/// Refuses a non-finite coordinate. The pair arity is carried by the
570/// argument type, so a wrong-length vertex cannot be expressed.
571pub fn create_texture_vertex(
572    tx: &mut Transaction,
573    schema: &Schema,
574    coordinates: [f64; 2],
575) -> StyleResult<EntityId> {
576    const ENTITY: &str = "IfcTextureVertex";
577    for value in coordinates {
578        if !value.is_finite() {
579            return Err(invalid_authoring(ENTITY, "Coordinates", format!("{value}")));
580        }
581    }
582    let values = vec![(
583        "Coordinates",
584        Value::List(coordinates.into_iter().map(Value::Real).collect()),
585    )];
586    Ok(tx.create(build_named(schema, ENTITY, values)?))
587}
588
589/// Stage an `IfcTextureVertexList`.
590///
591/// # Errors
592///
593/// Refuses an empty list and any non-finite coordinate.
594pub fn create_texture_vertex_list(
595    tx: &mut Transaction,
596    schema: &Schema,
597    coordinates: &[[f64; 2]],
598) -> StyleResult<EntityId> {
599    const ENTITY: &str = "IfcTextureVertexList";
600    if coordinates.is_empty() {
601        return Err(invalid_authoring(ENTITY, "TexCoordsList", "empty"));
602    }
603    for pair in coordinates {
604        for value in pair {
605            if !value.is_finite() {
606                return Err(invalid_authoring(
607                    ENTITY,
608                    "TexCoordsList",
609                    format!("{value}"),
610                ));
611            }
612        }
613    }
614    let values = vec![(
615        "TexCoordsList",
616        Value::List(
617            coordinates
618                .iter()
619                .map(|pair| Value::List(pair.iter().copied().map(Value::Real).collect()))
620                .collect(),
621        ),
622    )];
623    Ok(tx.create(build_named(schema, ENTITY, values)?))
624}
625
626/// Stage an `IfcColourRgbList`.
627///
628/// # Errors
629///
630/// Refuses an empty list and any channel outside `[0, 1]`: the inner
631/// members are `IfcNormalisedRatioMeasure`, not free reals.
632pub fn create_colour_rgb_list(
633    tx: &mut Transaction,
634    schema: &Schema,
635    colours: &[[f64; 3]],
636) -> StyleResult<EntityId> {
637    const ENTITY: &str = "IfcColourRgbList";
638    if colours.is_empty() {
639        return Err(invalid_authoring(ENTITY, "ColourList", "empty"));
640    }
641    for colour in colours {
642        for channel in colour {
643            if !channel.is_finite() || *channel < 0.0 || *channel > 1.0 {
644                return Err(invalid_authoring(
645                    ENTITY,
646                    "ColourList",
647                    format!("{channel}"),
648                ));
649            }
650        }
651    }
652    let values = vec![(
653        "ColourList",
654        Value::List(
655            colours
656                .iter()
657                .map(|colour| Value::List(colour.iter().copied().map(Value::Real).collect()))
658                .collect(),
659        ),
660    )];
661    Ok(tx.create(build_named(schema, ENTITY, values)?))
662}
663
664/// Stage an `IfcIndexedColourMap`.
665///
666/// # Errors
667///
668/// Refuses an empty colour index, a non-positive index (the members are
669/// `IfcPositiveInteger`, and STEP indices are one-based), and an opacity
670/// outside `[0, 1]`.
671pub fn create_indexed_colour_map(
672    tx: &mut Transaction,
673    schema: &Schema,
674    mapped_to: EntityId,
675    colours: EntityId,
676    colour_index: &[i64],
677    opacity: Option<f64>,
678) -> StyleResult<EntityId> {
679    const ENTITY: &str = "IfcIndexedColourMap";
680    if colour_index.is_empty() {
681        return Err(invalid_authoring(ENTITY, "ColourIndex", "empty"));
682    }
683    if let Some(index) = colour_index.iter().find(|index| **index < 1) {
684        return Err(invalid_authoring(ENTITY, "ColourIndex", format!("{index}")));
685    }
686    if let Some(value) = opacity {
687        if !value.is_finite() || !(0.0..=1.0).contains(&value) {
688            return Err(invalid_authoring(ENTITY, "Opacity", format!("{value}")));
689        }
690    }
691    let mut values = vec![("MappedTo", Value::Ref(mapped_to))];
692    if let Some(value) = opacity {
693        values.push(("Opacity", Value::Real(value)));
694    }
695    values.extend([
696        ("Colours", Value::Ref(colours)),
697        (
698            "ColourIndex",
699            Value::List(colour_index.iter().copied().map(Value::Integer).collect()),
700        ),
701    ]);
702    Ok(tx.create(build_named(schema, ENTITY, values)?))
703}
704
705/// Stage an `IfcPresentationLayerAssignment`.
706///
707/// # Errors
708///
709/// Refuses a blank name and an empty item set (ApplicableItems).
710pub fn create_presentation_layer_assignment(
711    tx: &mut Transaction,
712    schema: &Schema,
713    name: &str,
714    description: Option<&str>,
715    assigned_items: &[EntityId],
716    identifier: Option<&str>,
717) -> StyleResult<EntityId> {
718    const ENTITY: &str = "IfcPresentationLayerAssignment";
719    if name.trim().is_empty() {
720        return Err(invalid_authoring(ENTITY, "Name", name));
721    }
722    if assigned_items.is_empty() {
723        return Err(invalid_authoring(ENTITY, "AssignedItems", "empty"));
724    }
725    let mut values = vec![("Name", Value::Text(name.into()))];
726    optional_text(&mut values, "Description", description);
727    values.push((
728        "AssignedItems",
729        Value::List(assigned_items.iter().copied().map(Value::Ref).collect()),
730    ));
731    optional_text(&mut values, "Identifier", identifier);
732    Ok(tx.create(build_named(schema, ENTITY, values)?))
733}