1use crate::error::GeometryResult;
33use crate::slots::Slots;
34use ifc_model::{Entity, EntityId};
35
36pub(crate) mod plane_slot {
38 pub const BASIS_SURFACE: usize = 0;
40 pub const OUTER_BOUNDARY: usize = 1;
42 pub const INNER_BOUNDARIES: usize = 2;
44}
45
46pub(crate) mod surface_slot {
48 pub const BASIS_SURFACE: usize = 0;
50 pub const BOUNDARIES: usize = 1;
52 pub const IMPLICIT_OUTER: usize = 2;
54}
55
56pub(crate) mod trimmed_slot {
58 pub const BASIS_SURFACE: usize = 0;
60 pub const U1: usize = 1;
62 pub const V1: usize = 2;
64 pub const U2: usize = 3;
66 pub const V2: usize = 4;
68 pub const USENSE: usize = 5;
70 pub const VSENSE: usize = 6;
72}
73
74#[derive(Debug, Clone, Copy)]
76pub struct CurveBoundedPlane<'m> {
77 slots: Slots<'m>,
78}
79
80impl<'m> CurveBoundedPlane<'m> {
81 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
83 Self {
84 slots: Slots::new(id, entity),
85 }
86 }
87
88 pub fn id(&self) -> EntityId {
90 self.slots.id()
91 }
92
93 pub fn basis_surface_ref(&self) -> GeometryResult<EntityId> {
98 self.slots
99 .req_ref(plane_slot::BASIS_SURFACE, "BasisSurface")
100 }
101
102 pub fn outer_boundary_ref(&self) -> GeometryResult<EntityId> {
106 self.slots
107 .req_ref(plane_slot::OUTER_BOUNDARY, "OuterBoundary")
108 }
109
110 pub fn inner_boundary_refs(&self) -> Vec<EntityId> {
115 self.slots.opt_ref_list(plane_slot::INNER_BOUNDARIES)
116 }
117}
118
119#[derive(Debug, Clone, Copy)]
121pub struct CurveBoundedSurface<'m> {
122 slots: Slots<'m>,
123}
124
125impl<'m> CurveBoundedSurface<'m> {
126 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
128 Self {
129 slots: Slots::new(id, entity),
130 }
131 }
132
133 pub fn id(&self) -> EntityId {
135 self.slots.id()
136 }
137
138 pub fn basis_surface_ref(&self) -> GeometryResult<EntityId> {
140 self.slots
141 .req_ref(surface_slot::BASIS_SURFACE, "BasisSurface")
142 }
143
144 pub fn boundary_refs(&self) -> GeometryResult<Vec<EntityId>> {
151 let boundaries = self
152 .slots
153 .req_ref_list(surface_slot::BOUNDARIES, "Boundaries")?;
154 if boundaries.is_empty() {
155 return Err(self
156 .slots
157 .degenerate("Boundaries is empty; SET [1:?] requires a member"));
158 }
159 Ok(boundaries)
160 }
161
162 pub fn implicit_outer(&self) -> bool {
169 self.slots
170 .opt_bool(surface_slot::IMPLICIT_OUTER)
171 .unwrap_or(false)
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq)]
182pub struct TrimRectangle {
183 pub u1: f64,
185 pub v1: f64,
187 pub u2: f64,
189 pub v2: f64,
191 pub usense: bool,
193 pub vsense: bool,
195}
196
197impl TrimRectangle {
198 pub fn u_wraps(&self) -> bool {
205 self.usense == (self.u1 > self.u2)
206 }
207
208 pub fn v_wraps(&self) -> bool {
210 self.vsense == (self.v1 > self.v2)
211 }
212}
213
214#[derive(Debug, Clone, Copy)]
216pub struct RectangularTrimmedSurface<'m> {
217 slots: Slots<'m>,
218}
219
220impl<'m> RectangularTrimmedSurface<'m> {
221 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
223 Self {
224 slots: Slots::new(id, entity),
225 }
226 }
227
228 pub fn id(&self) -> EntityId {
230 self.slots.id()
231 }
232
233 pub fn basis_surface_ref(&self) -> GeometryResult<EntityId> {
235 self.slots
236 .req_ref(trimmed_slot::BASIS_SURFACE, "BasisSurface")
237 }
238
239 pub fn rectangle(&self) -> GeometryResult<TrimRectangle> {
245 let u1 = self.slots.req_f64(trimmed_slot::U1, "U1")?;
246 let v1 = self.slots.req_f64(trimmed_slot::V1, "V1")?;
247 let u2 = self.slots.req_f64(trimmed_slot::U2, "U2")?;
248 let v2 = self.slots.req_f64(trimmed_slot::V2, "V2")?;
249
250 if u1 == u2 {
251 return Err(self
252 .slots
253 .degenerate(format!("U1 and U2 are both {u1}; the patch has no extent")));
254 }
255 if v1 == v2 {
256 return Err(self
257 .slots
258 .degenerate(format!("V1 and V2 are both {v1}; the patch has no extent")));
259 }
260
261 Ok(TrimRectangle {
262 u1,
263 v1,
264 u2,
265 v2,
266 usense: self.slots.req_bool(trimmed_slot::USENSE, "Usense")?,
267 vsense: self.slots.req_bool(trimmed_slot::VSENSE, "Vsense")?,
268 })
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use ifc_model::Value;
276
277 fn refs(ids: &[u64]) -> Value {
278 Value::List(ids.iter().map(|i| Value::Ref(EntityId(*i))).collect())
279 }
280
281 fn trimmed(u1: f64, v1: f64, u2: f64, v2: f64, usense: bool, vsense: bool) -> Entity {
282 Entity::new(
283 "IFCRECTANGULARTRIMMEDSURFACE",
284 vec![
285 Value::Ref(EntityId(100)),
286 Value::Real(u1),
287 Value::Real(v1),
288 Value::Real(u2),
289 Value::Real(v2),
290 Value::Bool(usense),
291 Value::Bool(vsense),
292 ],
293 )
294 }
295
296 #[test]
297 fn a_curve_bounded_plane_separates_its_outline_from_its_holes() {
298 let e = Entity::new(
299 "IFCCURVEBOUNDEDPLANE",
300 vec![
301 Value::Ref(EntityId(100)),
302 Value::Ref(EntityId(101)),
303 refs(&[102, 103]),
304 ],
305 );
306 let view = CurveBoundedPlane::new(EntityId(1), &e);
307 assert_eq!(view.basis_surface_ref().unwrap(), EntityId(100));
308 assert_eq!(view.outer_boundary_ref().unwrap(), EntityId(101));
309 assert_eq!(
310 view.inner_boundary_refs(),
311 vec![EntityId(102), EntityId(103)]
312 );
313 }
314
315 #[test]
317 fn a_plane_with_no_holes_reports_an_empty_inner_boundary_list() {
318 let e = Entity::new(
319 "IFCCURVEBOUNDEDPLANE",
320 vec![
321 Value::Ref(EntityId(100)),
322 Value::Ref(EntityId(101)),
323 Value::List(vec![]),
324 ],
325 );
326 assert!(CurveBoundedPlane::new(EntityId(1), &e)
327 .inner_boundary_refs()
328 .is_empty());
329
330 let absent = Entity::new(
331 "IFCCURVEBOUNDEDPLANE",
332 vec![Value::Ref(EntityId(100)), Value::Ref(EntityId(101))],
333 );
334 assert!(CurveBoundedPlane::new(EntityId(1), &absent)
335 .inner_boundary_refs()
336 .is_empty());
337 }
338
339 #[test]
342 fn implicit_outer_makes_every_listed_boundary_a_hole() {
343 let implicit = Entity::new(
344 "IFCCURVEBOUNDEDSURFACE",
345 vec![
346 Value::Ref(EntityId(100)),
347 refs(&[101, 102]),
348 Value::Bool(true),
349 ],
350 );
351 let view = CurveBoundedSurface::new(EntityId(1), &implicit);
352 assert!(view.implicit_outer());
353 assert_eq!(view.boundary_refs().unwrap().len(), 2);
354
355 let explicit = Entity::new(
356 "IFCCURVEBOUNDEDSURFACE",
357 vec![
358 Value::Ref(EntityId(100)),
359 refs(&[101, 102]),
360 Value::Bool(false),
361 ],
362 );
363 assert!(!CurveBoundedSurface::new(EntityId(1), &explicit).implicit_outer());
364 }
365
366 #[test]
369 fn an_absent_implicit_outer_defaults_to_false() {
370 let e = Entity::new(
371 "IFCCURVEBOUNDEDSURFACE",
372 vec![Value::Ref(EntityId(100)), refs(&[101])],
373 );
374 assert!(!CurveBoundedSurface::new(EntityId(1), &e).implicit_outer());
375 }
376
377 #[test]
378 fn a_curve_bounded_surface_with_no_boundaries_is_degenerate() {
379 let e = Entity::new(
380 "IFCCURVEBOUNDEDSURFACE",
381 vec![Value::Ref(EntityId(100)), Value::List(vec![])],
382 );
383 assert!(CurveBoundedSurface::new(EntityId(1), &e)
384 .boundary_refs()
385 .is_err());
386 }
387
388 #[test]
391 fn trim_parameters_are_read_in_u1_v1_u2_v2_declaration_order() {
392 let e = trimmed(0.0, 1.0, 2.0, 3.0, true, true);
393 let rect = RectangularTrimmedSurface::new(EntityId(1), &e)
394 .rectangle()
395 .unwrap();
396 assert_eq!(rect.u1, 0.0);
397 assert_eq!(rect.v1, 1.0);
398 assert_eq!(rect.u2, 2.0);
399 assert_eq!(rect.v2, 3.0);
400 }
401
402 #[test]
405 fn descending_trim_bounds_are_preserved_not_normalised() {
406 let e = trimmed(350.0, 0.0, 10.0, 1.0, true, true);
407 let rect = RectangularTrimmedSurface::new(EntityId(1), &e)
408 .rectangle()
409 .unwrap();
410 assert_eq!(rect.u1, 350.0);
411 assert_eq!(rect.u2, 10.0);
412 assert!(rect.u_wraps());
413 assert!(!rect.v_wraps());
414 }
415
416 #[test]
419 fn the_sense_flags_distinguish_otherwise_identical_rectangles() {
420 let a = trimmed(0.0, 0.0, 90.0, 1.0, true, true);
421 let b = trimmed(0.0, 0.0, 90.0, 1.0, false, true);
422 let rect_a = RectangularTrimmedSurface::new(EntityId(1), &a)
423 .rectangle()
424 .unwrap();
425 let rect_b = RectangularTrimmedSurface::new(EntityId(1), &b)
426 .rectangle()
427 .unwrap();
428 assert_ne!(rect_a, rect_b);
429 assert!(!rect_a.u_wraps());
430 assert!(rect_b.u_wraps());
431 }
432
433 #[test]
434 fn a_zero_extent_trim_rectangle_is_degenerate_and_names_the_direction() {
435 let flat_u = trimmed(1.0, 0.0, 1.0, 2.0, true, true);
436 let err = RectangularTrimmedSurface::new(EntityId(4), &flat_u)
437 .rectangle()
438 .unwrap_err();
439 assert!(err.to_string().contains("U1 and U2"), "got: {err}");
440
441 let flat_v = trimmed(0.0, 5.0, 1.0, 5.0, true, true);
442 let err = RectangularTrimmedSurface::new(EntityId(4), &flat_v)
443 .rectangle()
444 .unwrap_err();
445 assert!(err.to_string().contains("V1 and V2"), "got: {err}");
446 }
447
448 #[test]
449 fn trim_parameters_read_through_parameter_value_wrappers() {
450 let e = Entity::new(
451 "IFCRECTANGULARTRIMMEDSURFACE",
452 vec![
453 Value::Ref(EntityId(100)),
454 Value::Typed {
455 type_name: "IFCPARAMETERVALUE".into(),
456 value: Box::new(Value::Real(0.0)),
457 },
458 Value::Real(0.0),
459 Value::Typed {
460 type_name: "IFCPARAMETERVALUE".into(),
461 value: Box::new(Value::Real(1.0)),
462 },
463 Value::Real(1.0),
464 Value::Bool(true),
465 Value::Bool(true),
466 ],
467 );
468 let rect = RectangularTrimmedSurface::new(EntityId(1), &e)
469 .rectangle()
470 .unwrap();
471 assert_eq!(rect.u2, 1.0);
472 }
473}