1use 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#[derive(Debug, Clone, Default)]
58pub struct AnnotationDraft<'a> {
59 pub global_id: &'a str,
61 pub owner_history: Option<EntityId>,
63 pub name: Option<&'a str>,
65 pub description: Option<&'a str>,
67 pub object_type: Option<&'a str>,
70 pub object_placement: Option<EntityId>,
72 pub representation: Option<EntityId>,
74 pub predefined_type: Option<AnnotationType>,
76}
77
78#[derive(Debug, Clone, Copy)]
81pub struct TextLiteralDraft<'a> {
82 pub literal: &'a str,
84 pub placement: EntityId,
86 pub path: TextPath,
88}
89
90#[derive(Debug, Clone, Copy)]
93pub struct TextLiteralWithExtentDraft<'a> {
94 pub literal: &'a str,
96 pub placement: EntityId,
98 pub path: TextPath,
100 pub extent: EntityId,
102 pub box_alignment: BoxAlignment,
104}
105
106#[derive(Debug, Clone)]
109pub struct AnnotationFillAreaDraft {
110 pub outer_boundary: EntityId,
112 pub inner_boundaries: Vec<EntityId>,
115}
116
117pub 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
173pub 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
196pub 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
227pub 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
254pub 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 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
322fn 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}