Skip to main content

ifc_style/authoring/
mod.rs

1//! Transaction-staged authoring for presentation and annotation entities.
2
3use std::sync::Arc;
4
5use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
6use ifc_schema::Schema;
7
8use crate::annotation::{AnnotationType, BoxAlignment, TextPath};
9use crate::error::{StyleError, StyleResult};
10
11mod external;
12mod fill;
13mod light;
14mod presentation;
15mod surface;
16mod texture;
17
18pub use surface::{
19    create_colour_rgb, create_presentation_layer_with_style, create_styled_item,
20    create_surface_style, create_surface_style_rendering, create_surface_style_shading,
21    ColourRgbDraft, PresentationLayerDraft, StyledItemDraft, SurfaceStyleDraft,
22    SurfaceStyleRenderingDraft, SurfaceStyleShadingDraft,
23};
24
25pub use external::{
26    create_blob_texture, create_externally_defined_style, create_indexed_polygonal_texture_map,
27    create_indexed_triangle_texture_map, create_light_distribution_data,
28    create_light_intensity_distribution, create_surface_style_with_textures, create_text_style,
29    create_text_style_text_model, create_texture_coordinate_indices,
30    create_texture_coordinate_indices_with_voids, BlobTextureDraft, ExternalStyleKind, SizeValue,
31    TextModelDraft,
32};
33pub use fill::{create_fill_area_style_hatching, create_fill_area_style_tiles, HatchLineDistance};
34pub use light::{
35    create_light_source_ambient, create_light_source_directional, create_light_source_goniometric,
36    create_light_source_positional, create_light_source_spot, create_surface_style_lighting,
37    create_surface_style_refraction, Attenuation, GoniometricLight, LightSourceDraft, PointLight,
38    SpotCone,
39};
40pub use presentation::{
41    create_colour_rgb_list, create_curve_style, create_curve_style_font,
42    create_curve_style_font_and_scaling, create_curve_style_font_pattern,
43    create_draughting_predefined_colour, create_draughting_predefined_curve_font,
44    create_fill_area_style, create_indexed_colour_map, create_pixel_texture,
45    create_presentation_layer_assignment, create_text_style_font_model,
46    create_text_style_for_defined_font, create_texture_vertex, create_texture_vertex_list,
47    CurveStyleDraft, CurveWidth, FillStyleKind, PixelTextureDraft, PREDEFINED_COLOUR_NAMES,
48    PREDEFINED_CURVE_FONT_NAMES,
49};
50pub use texture::{
51    create_image_texture, create_texture_coordinate_generator, create_texture_map,
52    ImageTextureDraft,
53};
54
55/// Draft input for [`create_annotation`]: the writable attributes of a new
56/// `IfcAnnotation`.
57#[derive(Debug, Clone, Default)]
58pub struct AnnotationDraft<'a> {
59    /// The `GlobalId` (IFC GUID); must parse as a valid base64-like GUID.
60    pub global_id: &'a str,
61    /// The `OwnerHistory` reference, when supplied.
62    pub owner_history: Option<EntityId>,
63    /// The `Name` attribute, when supplied.
64    pub name: Option<&'a str>,
65    /// The `Description` attribute, when supplied.
66    pub description: Option<&'a str>,
67    /// The `ObjectType` attribute. Required (non-empty) when `predefined_type`
68    /// is `AnnotationType::UserDefined`.
69    pub object_type: Option<&'a str>,
70    /// The `ObjectPlacement` reference, when supplied.
71    pub object_placement: Option<EntityId>,
72    /// The `Representation` reference, when supplied.
73    pub representation: Option<EntityId>,
74    /// The IFC4X3 `PredefinedType`, when supplied.
75    pub predefined_type: Option<AnnotationType>,
76}
77
78/// Draft input for [`create_text_literal`]: the writable attributes of a new
79/// `IfcTextLiteral`.
80#[derive(Debug, Clone, Copy)]
81pub struct TextLiteralDraft<'a> {
82    /// The `Literal` attribute; must be non-empty.
83    pub literal: &'a str,
84    /// The `Placement` reference to an `IfcPlacement`.
85    pub placement: EntityId,
86    /// The `Path` attribute.
87    pub path: TextPath,
88}
89
90/// Draft input for [`create_text_literal_with_extent`]: the writable
91/// attributes of a new `IfcTextLiteralWithExtent`.
92#[derive(Debug, Clone, Copy)]
93pub struct TextLiteralWithExtentDraft<'a> {
94    /// The `Literal` attribute; must be non-empty.
95    pub literal: &'a str,
96    /// The `Placement` reference to an `IfcPlacement`.
97    pub placement: EntityId,
98    /// The `Path` attribute.
99    pub path: TextPath,
100    /// The `Extent` reference to an `IfcPlanarExtent`.
101    pub extent: EntityId,
102    /// The `BoxAlignment` attribute.
103    pub box_alignment: BoxAlignment,
104}
105
106/// Draft input for [`create_annotation_fill_area`]: the writable attributes
107/// of a new `IfcAnnotationFillArea`.
108#[derive(Debug, Clone)]
109pub struct AnnotationFillAreaDraft {
110    /// The `OuterBoundary` reference to an `IfcCurve`.
111    pub outer_boundary: EntityId,
112    /// The `InnerBoundaries` references, if any; an empty list is written
113    /// as the IFC null value.
114    pub inner_boundaries: Vec<EntityId>,
115}
116
117/// Stage a new `IfcAnnotation` in `tx`. Fails if `GlobalId` does not parse as
118/// a GUID, if `PredefinedType` is `USERDEFINED` with an empty `ObjectType`,
119/// or if any referenced entity does not resolve to its expected IFC type.
120pub fn create_annotation(
121    tx: &mut Transaction,
122    model: &Model,
123    schema: &Schema,
124    draft: AnnotationDraft<'_>,
125) -> StyleResult<EntityId> {
126    if ifc_model::guid::Guid::parse(draft.global_id).is_none() {
127        return Err(invalid_authoring(
128            "IfcAnnotation",
129            "GlobalId",
130            draft.global_id,
131        ));
132    }
133    if draft.predefined_type == Some(AnnotationType::UserDefined)
134        && draft
135            .object_type
136            .is_none_or(|value| value.trim().is_empty())
137    {
138        return Err(invalid_authoring(
139            "IfcAnnotation",
140            "ObjectType",
141            "USERDEFINED requires a non-empty ObjectType",
142        ));
143    }
144    validate_optional_ref(tx, model, schema, draft.owner_history, "IfcOwnerHistory")?;
145    validate_optional_ref(
146        tx,
147        model,
148        schema,
149        draft.object_placement,
150        "IfcObjectPlacement",
151    )?;
152    validate_optional_ref(
153        tx,
154        model,
155        schema,
156        draft.representation,
157        "IfcProductRepresentation",
158    )?;
159
160    let mut values = vec![("GlobalId", text(draft.global_id))];
161    optional_reference(&mut values, "OwnerHistory", draft.owner_history);
162    optional_text(&mut values, "Name", draft.name);
163    optional_text(&mut values, "Description", draft.description);
164    optional_text(&mut values, "ObjectType", draft.object_type);
165    optional_reference(&mut values, "ObjectPlacement", draft.object_placement);
166    optional_reference(&mut values, "Representation", draft.representation);
167    if let Some(value) = draft.predefined_type {
168        values.push(("PredefinedType", enumeration(value.as_ifc())));
169    }
170    Ok(tx.create(build_named(schema, "IfcAnnotation", values)?))
171}
172
173/// Stage a new `IfcTextLiteral` in `tx`. Fails if `literal` is empty or
174/// `placement` does not resolve to an `IfcPlacement`.
175pub fn create_text_literal(
176    tx: &mut Transaction,
177    model: &Model,
178    schema: &Schema,
179    draft: TextLiteralDraft<'_>,
180) -> StyleResult<EntityId> {
181    if draft.literal.is_empty() {
182        return Err(invalid_authoring("IfcTextLiteral", "Literal", "empty"));
183    }
184    validate_ref(tx, model, schema, draft.placement, "IfcPlacement")?;
185    Ok(tx.create(build_named(
186        schema,
187        "IfcTextLiteral",
188        vec![
189            ("Literal", text(draft.literal)),
190            ("Placement", Value::Ref(draft.placement)),
191            ("Path", enumeration(draft.path.as_ifc())),
192        ],
193    )?))
194}
195
196/// Stage a new `IfcTextLiteralWithExtent` in `tx`. Fails if `literal` is
197/// empty, or if `placement`/`extent` do not resolve to their expected types.
198pub fn create_text_literal_with_extent(
199    tx: &mut Transaction,
200    model: &Model,
201    schema: &Schema,
202    draft: TextLiteralWithExtentDraft<'_>,
203) -> StyleResult<EntityId> {
204    if draft.literal.is_empty() {
205        return Err(invalid_authoring(
206            "IfcTextLiteralWithExtent",
207            "Literal",
208            "empty",
209        ));
210    }
211
212    validate_ref(tx, model, schema, draft.placement, "IfcPlacement")?;
213    validate_ref(tx, model, schema, draft.extent, "IfcPlanarExtent")?;
214    Ok(tx.create(build_named(
215        schema,
216        "IfcTextLiteralWithExtent",
217        vec![
218            ("Literal", text(draft.literal)),
219            ("Placement", Value::Ref(draft.placement)),
220            ("Path", enumeration(draft.path.as_ifc())),
221            ("Extent", Value::Ref(draft.extent)),
222            ("BoxAlignment", text(draft.box_alignment.as_ifc())),
223        ],
224    )?))
225}
226
227/// Stage a new `IfcAnnotationFillArea` in `tx`. Fails if `outer_boundary` or
228/// any of `inner_boundaries` does not resolve to an `IfcCurve`.
229pub fn create_annotation_fill_area(
230    tx: &mut Transaction,
231    model: &Model,
232    schema: &Schema,
233    draft: AnnotationFillAreaDraft,
234) -> StyleResult<EntityId> {
235    validate_ref(tx, model, schema, draft.outer_boundary, "IfcCurve")?;
236    for inner in &draft.inner_boundaries {
237        validate_ref(tx, model, schema, *inner, "IfcCurve")?;
238    }
239    let inner = if draft.inner_boundaries.is_empty() {
240        Value::Null
241    } else {
242        Value::List(draft.inner_boundaries.into_iter().map(Value::Ref).collect())
243    };
244    Ok(tx.create(build_named(
245        schema,
246        "IfcAnnotationFillArea",
247        vec![
248            ("OuterBoundary", Value::Ref(draft.outer_boundary)),
249            ("InnerBoundaries", inner),
250        ],
251    )?))
252}
253
254/// Pick whichever spelling of an attribute the target schema declares.
255///
256/// A handful of attributes were renamed between IFC4 and IFC4X3 without
257/// moving slot or changing type: `IfcFillAreaStyle.ModelorDraughting`
258/// became `ModelOrDraughting`, and
259/// `IfcCurveStyleFontAndScaling.CurveFont` became `CurveStyleFont`.
260/// Writing a hardcoded spelling makes the writer refuse its own output
261/// under the other schema, so the candidates are resolved against the
262/// schema in use. Falls back to the first candidate so an unknown entity
263/// still produces the usual `build_named` error rather than a silent miss.
264/// Stage an `IfcPlanarExtent`, or its `IfcPlanarBox` subtype.
265///
266/// The extent is a rectangular presentation area: two sizes and, for
267/// the box form, the placement that positions it. Sizes are
268/// `IfcLengthMeasure`, which admits negatives in the type system;
269/// a negative or non-finite extent describes no area, so it is
270/// refused here rather than written for a reader to puzzle over.
271///
272/// Passing a placement selects `IfcPlanarBox`; omitting it stages the
273/// plain extent. The two share slots 0 and 1, so the subtype only ever
274/// adds.
275///
276/// # Errors
277///
278/// Refuses a non-finite or non-positive `SizeInX`/`SizeInY`, and a
279/// `placement` that is not an `IfcAxis2Placement2D`/`3D`.
280pub fn create_planar_extent(
281    tx: &mut Transaction,
282    model: &Model,
283    schema: &Schema,
284    size_in_x: f64,
285    size_in_y: f64,
286    placement: Option<EntityId>,
287) -> StyleResult<EntityId> {
288    let entity = if placement.is_some() {
289        "IfcPlanarBox"
290    } else {
291        "IfcPlanarExtent"
292    };
293    for (attribute, value) in [("SizeInX", size_in_x), ("SizeInY", size_in_y)] {
294        if !value.is_finite() || value <= 0.0 {
295            return Err(invalid_authoring(entity, attribute, format!("{value}")));
296        }
297    }
298    let mut values = vec![
299        ("SizeInX", Value::Real(size_in_x)),
300        ("SizeInY", Value::Real(size_in_y)),
301    ];
302    if let Some(placement) = placement {
303        // `IfcAxis2Placement` is a SELECT over the 2D and 3D forms, not
304        // a supertype, so `validate_ref`'s `is_a` check rejects both
305        // members. Each concrete form is checked instead.
306        let placement_type = schema_placement_type(tx, model, placement)?;
307        if !matches!(
308            placement_type.as_str(),
309            "IFCAXIS2PLACEMENT2D" | "IFCAXIS2PLACEMENT3D"
310        ) {
311            return Err(StyleError::ReferenceType {
312                target: placement,
313                expected: "IfcAxis2Placement",
314                actual: placement_type,
315            });
316        }
317        values.push(("Placement", Value::Ref(placement)));
318    }
319    Ok(tx.create(build_named(schema, entity, values)?))
320}
321
322/// Resolve the type name of a staged or committed entity.
323///
324/// A `Transaction` cannot be read back, so a reference to an
325/// entity created earlier in the same transaction is only visible
326/// in its edit list.
327fn schema_placement_type(tx: &Transaction, model: &Model, target: EntityId) -> StyleResult<String> {
328    tx.edits()
329        .iter()
330        .rev()
331        .find_map(|edit| match edit {
332            Edit::Create { id, entity } if *id == target => Some(entity.type_name.to_string()),
333            _ => None,
334        })
335        .or_else(|| model.get(target).map(|e| e.type_name.to_string()))
336        .ok_or(StyleError::DanglingReference {
337            source_id: EntityId(0),
338            target,
339        })
340}
341
342pub(crate) fn schema_attribute(
343    schema: &Schema,
344    entity: &str,
345    candidates: [&'static str; 2],
346) -> &'static str {
347    candidates
348        .into_iter()
349        .find(|candidate| {
350            schema
351                .attributes(entity)
352                .iter()
353                .any(|attribute| attribute.name.eq_ignore_ascii_case(candidate))
354        })
355        .unwrap_or(candidates[0])
356}
357
358pub(crate) fn build_named(
359    schema: &Schema,
360    entity: &'static str,
361    values: Vec<(&'static str, Value)>,
362) -> StyleResult<Entity> {
363    let declared = schema.attributes(entity);
364    if declared.is_empty() && schema.entity(entity).is_none() {
365        return Err(StyleError::UnsupportedEntity {
366            schema: schema.name().to_owned(),
367            entity,
368        });
369    }
370    let mut slots = vec![Value::Null; declared.len()];
371    let mut filled = vec![false; declared.len()];
372    for (name, value) in values {
373        let Some(index) = declared
374            .iter()
375            .position(|attribute| attribute.name.eq_ignore_ascii_case(name))
376        else {
377            return Err(StyleError::UnsupportedAttribute {
378                schema: schema.name().to_owned(),
379                entity,
380                attribute: name,
381            });
382        };
383        slots[index] = value;
384        filled[index] = true;
385    }
386    for (index, attribute) in declared.iter().enumerate() {
387        if !filled[index] && !attribute.optional {
388            return Err(StyleError::AuthoringInvalid {
389                entity,
390                attribute: "required attribute",
391                value: attribute.name.clone(),
392            });
393        }
394    }
395    Ok(Entity::new(entity.to_ascii_uppercase(), slots))
396}
397
398pub(crate) fn validate_ref(
399    tx: &Transaction,
400    model: &Model,
401    schema: &Schema,
402    target: EntityId,
403    expected: &'static str,
404) -> StyleResult<()> {
405    let type_name = tx
406        .edits()
407        .iter()
408        .rev()
409        .find_map(|edit| match edit {
410            Edit::Create { id, entity } if *id == target => Some(entity.type_name.as_ref()),
411            _ => None,
412        })
413        .or_else(|| model.get(target).map(|entity| entity.type_name.as_ref()))
414        .ok_or(StyleError::DanglingReference {
415            source_id: EntityId(0),
416            target,
417        })?;
418    if !schema.is_a(type_name, expected) {
419        return Err(StyleError::ReferenceType {
420            target,
421            expected,
422            actual: type_name.to_owned(),
423        });
424    }
425    Ok(())
426}
427
428pub(crate) fn validate_optional_ref(
429    tx: &Transaction,
430    model: &Model,
431    schema: &Schema,
432    target: Option<EntityId>,
433    expected: &'static str,
434) -> StyleResult<()> {
435    if let Some(target) = target {
436        validate_ref(tx, model, schema, target, expected)?;
437    }
438    Ok(())
439}
440
441fn text(value: &str) -> Value {
442    Value::Text(Arc::from(value))
443}
444
445pub(crate) fn enumeration(value: &str) -> Value {
446    Value::Enum(Arc::from(value.to_ascii_uppercase()))
447}
448
449fn optional_text(values: &mut Vec<(&'static str, Value)>, name: &'static str, value: Option<&str>) {
450    if let Some(value) = value {
451        values.push((name, text(value)));
452    }
453}
454
455fn optional_reference(
456    values: &mut Vec<(&'static str, Value)>,
457    name: &'static str,
458    value: Option<EntityId>,
459) {
460    if let Some(value) = value {
461        values.push((name, Value::Ref(value)));
462    }
463}
464
465pub(crate) fn invalid_authoring(
466    entity: &'static str,
467    attribute: &'static str,
468    value: impl ToString,
469) -> StyleError {
470    StyleError::AuthoringInvalid {
471        entity,
472        attribute,
473        value: value.to_string(),
474    }
475}
476
477pub(crate) fn validate_ratio(
478    entity: &'static str,
479    attribute: &'static str,
480    value: f64,
481) -> StyleResult<()> {
482    if value.is_finite() && (0.0..=1.0).contains(&value) {
483        Ok(())
484    } else {
485        Err(invalid_authoring(entity, attribute, value))
486    }
487}