1use ifc_model::{Entity, EntityId, Transaction, Value};
22
23use crate::error::GeometryError;
24
25use super::{invalid, require_finite};
26
27const LIST_2D: &str = "IFCCARTESIANPOINTLIST2D";
29const LIST_3D: &str = "IFCCARTESIANPOINTLIST3D";
30
31mod slot {
33 pub const COORD_LIST: usize = 0;
35 pub const TAG_LIST: usize = 1;
37 pub const COORDINATES: usize = 0;
39 pub const TRI_NORMALS: usize = 1;
41 pub const TRI_CLOSED: usize = 2;
43 pub const TRI_COORD_INDEX: usize = 3;
45 pub const TRI_PN_INDEX: usize = 4;
47 pub const POLY_CLOSED: usize = 1;
49 pub const POLY_FACES: usize = 2;
51 pub const POLY_PN_INDEX: usize = 3;
53 pub const FACE_COORD_INDEX: usize = 0;
55 pub const FACE_INNER: usize = 1;
57}
58
59fn index_1based(
65 type_name: &'static str,
66 attribute: &'static str,
67 value: usize,
68 point_count: usize,
69) -> Result<Value, GeometryError> {
70 if value >= point_count {
71 return Err(invalid(
72 type_name,
73 attribute,
74 format!("index {value} is outside a point list of {point_count}"),
75 ));
76 }
77 Ok(Value::Integer(value as i64 + 1))
79}
80
81pub fn cartesian_point_list_3d(
92 tx: &mut Transaction,
93 points: &[[f64; 3]],
94 tags: Option<&[&str]>,
95) -> Result<EntityId, GeometryError> {
96 point_list(
97 tx,
98 LIST_3D,
99 points.iter().map(|p| p.as_slice()),
100 points.len(),
101 tags,
102 )
103}
104
105pub fn cartesian_point_list_2d(
111 tx: &mut Transaction,
112 points: &[[f64; 2]],
113 tags: Option<&[&str]>,
114) -> Result<EntityId, GeometryError> {
115 point_list(
116 tx,
117 LIST_2D,
118 points.iter().map(|p| p.as_slice()),
119 points.len(),
120 tags,
121 )
122}
123
124fn point_list<'p>(
126 tx: &mut Transaction,
127 type_name: &'static str,
128 rows: impl Iterator<Item = &'p [f64]>,
129 count: usize,
130 tags: Option<&[&str]>,
131) -> Result<EntityId, GeometryError> {
132 if count == 0 {
133 return Err(invalid(
134 type_name,
135 "CoordList",
136 "expected at least one point",
137 ));
138 }
139 let mut coords = Vec::with_capacity(count);
140 for row in rows {
141 require_finite(type_name, "CoordList", row)?;
142 coords.push(Value::List(row.iter().copied().map(Value::Real).collect()));
143 }
144 let mut attrs = vec![Value::Null; slot::COORD_LIST + 1];
150 attrs[slot::COORD_LIST] = Value::List(coords);
151 if let Some(tags) = tags {
152 if tags.len() != count {
153 return Err(invalid(
154 type_name,
155 "TagList",
156 format!("{} tags for {count} points", tags.len()),
157 ));
158 }
159 attrs.resize(slot::TAG_LIST + 1, Value::Null);
160 attrs[slot::TAG_LIST] =
161 Value::List(tags.iter().map(|t| Value::Text((*t).into())).collect());
162 }
163 Ok(tx.create(Entity::new(type_name, attrs)))
164}
165
166#[derive(Debug, Default, Clone, Copy)]
168pub struct TriangulatedExtras<'a> {
169 pub closed: Option<bool>,
174 pub normals: Option<&'a [[f64; 3]]>,
176 pub pn_index: Option<&'a [usize]>,
178}
179
180pub fn triangulated_face_set(
190 tx: &mut Transaction,
191 coordinates: EntityId,
192 point_count: usize,
193 triangles: &[[usize; 3]],
194 extras: TriangulatedExtras<'_>,
195) -> Result<EntityId, GeometryError> {
196 let attrs = triangulated_attrs(
197 "IFCTRIANGULATEDFACESET",
198 5,
199 coordinates,
200 point_count,
201 triangles,
202 extras,
203 )?;
204 Ok(tx.create(Entity::new("IFCTRIANGULATEDFACESET", attrs)))
205}
206
207fn triangulated_attrs(
212 type_name: &'static str,
213 arity: usize,
214 coordinates: EntityId,
215 point_count: usize,
216 triangles: &[[usize; 3]],
217 extras: TriangulatedExtras<'_>,
218) -> Result<Vec<Value>, GeometryError> {
219 if triangles.is_empty() {
220 return Err(invalid(
221 type_name,
222 "CoordIndex",
223 "expected at least one triangle",
224 ));
225 }
226 let mut indexed = Vec::with_capacity(triangles.len());
227 for triangle in triangles {
228 let mut row = Vec::with_capacity(3);
229 for value in triangle {
230 row.push(index_1based(type_name, "CoordIndex", *value, point_count)?);
231 }
232 indexed.push(Value::List(row));
233 }
234
235 let mut attrs = vec![Value::Null; arity];
236 attrs[slot::COORDINATES] = Value::Ref(coordinates);
237 attrs[slot::TRI_COORD_INDEX] = Value::List(indexed);
238 if let Some(closed) = extras.closed {
239 attrs[slot::TRI_CLOSED] = Value::Bool(closed);
240 }
241 if let Some(normals) = extras.normals {
242 let mut rows = Vec::with_capacity(normals.len());
243 for normal in normals {
244 require_finite(type_name, "Normals", normal)?;
245 rows.push(Value::List(
246 normal.iter().copied().map(Value::Real).collect(),
247 ));
248 }
249 attrs[slot::TRI_NORMALS] = Value::List(rows);
250 }
251 if let Some(pn) = extras.pn_index {
252 let mut rows = Vec::with_capacity(pn.len());
253 for value in pn {
254 rows.push(index_1based(type_name, "PnIndex", *value, point_count)?);
255 }
256 attrs[slot::TRI_PN_INDEX] = Value::List(rows);
257 }
258 Ok(attrs)
259}
260
261pub fn indexed_polygonal_face(
270 tx: &mut Transaction,
271 outer: &[usize],
272 point_count: usize,
273) -> Result<EntityId, GeometryError> {
274 const T: &str = "IFCINDEXEDPOLYGONALFACE";
275 let indices = face_loop(T, "CoordIndex", outer, point_count)?;
276 let mut attrs = vec![Value::Null];
277 attrs[slot::FACE_COORD_INDEX] = indices;
278 Ok(tx.create(Entity::new(T, attrs)))
279}
280
281pub fn indexed_polygonal_face_with_voids(
295 tx: &mut Transaction,
296 outer: &[usize],
297 voids: &[&[usize]],
298 point_count: usize,
299) -> Result<EntityId, GeometryError> {
300 const T: &str = "IFCINDEXEDPOLYGONALFACEWITHVOIDS";
301 if voids.is_empty() {
302 return Err(invalid(
303 T,
304 "InnerCoordIndices",
305 "expected at least one void; a face without voids is an IfcIndexedPolygonalFace",
306 ));
307 }
308 let outer_indices = face_loop(T, "CoordIndex", outer, point_count)?;
309 let mut inner = Vec::with_capacity(voids.len());
310 for loop_indices in voids {
311 inner.push(face_loop(
312 T,
313 "InnerCoordIndices",
314 loop_indices,
315 point_count,
316 )?);
317 }
318 let mut attrs = vec![Value::Null; 2];
319 attrs[slot::FACE_COORD_INDEX] = outer_indices;
320 attrs[slot::FACE_INNER] = Value::List(inner);
321 Ok(tx.create(Entity::new(T, attrs)))
322}
323
324fn face_loop(
326 type_name: &'static str,
327 attribute: &'static str,
328 indices: &[usize],
329 point_count: usize,
330) -> Result<Value, GeometryError> {
331 if indices.len() < 3 {
332 return Err(invalid(
333 type_name,
334 attribute,
335 format!("expected at least 3 vertices, got {}", indices.len()),
336 ));
337 }
338 let mut row = Vec::with_capacity(indices.len());
339 for value in indices {
340 row.push(index_1based(type_name, attribute, *value, point_count)?);
341 }
342 Ok(Value::List(row))
343}
344
345pub fn polygonal_face_set(
357 tx: &mut Transaction,
358 coordinates: EntityId,
359 point_count: usize,
360 faces: &[EntityId],
361 closed: Option<bool>,
362 pn_index: Option<&[usize]>,
363) -> Result<EntityId, GeometryError> {
364 const T: &str = "IFCPOLYGONALFACESET";
365 if faces.is_empty() {
366 return Err(invalid(T, "Faces", "expected at least one face"));
367 }
368 let mut seen = faces.to_vec();
369 seen.sort_unstable();
370 seen.dedup();
371 if seen.len() != faces.len() {
372 return Err(invalid(
373 T,
374 "Faces",
375 "expected a UNIQUE face list, got a repeated face",
376 ));
377 }
378
379 let mut attrs = vec![Value::Null; 4];
380 attrs[slot::COORDINATES] = Value::Ref(coordinates);
381 attrs[slot::POLY_FACES] = Value::List(faces.iter().copied().map(Value::Ref).collect());
382 if let Some(closed) = closed {
383 attrs[slot::POLY_CLOSED] = Value::Bool(closed);
384 }
385 if let Some(pn) = pn_index {
386 let mut rows = Vec::with_capacity(pn.len());
387 for value in pn {
388 rows.push(index_1based(T, "PnIndex", *value, point_count)?);
389 }
390 attrs[slot::POLY_PN_INDEX] = Value::List(rows);
391 }
392 Ok(tx.create(Entity::new(T, attrs)))
393}
394
395pub fn triangulated_irregular_network(
410 tx: &mut Transaction,
411 coordinates: EntityId,
412 point_count: usize,
413 triangles: &[[usize; 3]],
414 extras: TriangulatedExtras<'_>,
415 flags: &[i64],
416) -> Result<EntityId, GeometryError> {
417 const T: &str = "IFCTRIANGULATEDIRREGULARNETWORK";
418 const FLAGS: usize = 5;
419 if flags.len() != triangles.len() {
420 return Err(invalid(
421 T,
422 "Flags",
423 format!(
424 "expected one flag per triangle: {} triangles, {} flags",
425 triangles.len(),
426 flags.len()
427 ),
428 ));
429 }
430 let mut attrs = triangulated_attrs(T, 6, coordinates, point_count, triangles, extras)?;
431 attrs[slot::TRI_CLOSED] = Value::Bool(false);
433 attrs[FLAGS] = Value::List(flags.iter().copied().map(Value::Integer).collect());
434 Ok(tx.create(Entity::new(T, attrs)))
435}