ifc_geometry/input/openings.rs
1//! Which openings void a host element (`IfcRelVoidsElement`).
2//!
3//! # Why this is geometry input and not a spatial query
4//!
5//! `ifc-spatial` indexes the relation for navigation, but only this crate
6//! needs it to decide what a product's NET shape is: the Body representation
7//! of a wall is the gross wall, and the file never authors the net one. So
8//! the relation is read here, as a kernel-free slot read, and `lower` turns it
9//! into boolean differences.
10//!
11//! # The slots are stable
12//!
13//! `RelatingBuildingElement` is slot 4 and `RelatedOpeningElement` slot 5 in
14//! IFC2X3 (under `IfcRelConnects`), IFC4 and IFC4X3 (under
15//! `IfcRelDecomposes`): both supertypes contribute exactly the four
16//! `IfcRoot` attributes. `tests/context_slots.rs` asserts that against the
17//! shipped schemas, following ADR 0008.
18
19use std::collections::BTreeMap;
20
21use ifc_model::{EntityId, Model, Value};
22
23use crate::slots::Slots;
24
25/// Absolute slots on `IfcRelVoidsElement`.
26pub mod slot {
27 /// `RelatingBuildingElement`: the host that owns the void.
28 pub const RELATING_BUILDING_ELEMENT: usize = 4;
29 /// `RelatedOpeningElement`: the `IfcFeatureElementSubtraction` removed.
30 pub const RELATED_OPENING_ELEMENT: usize = 5;
31}
32
33/// The type name the relation is stored under. It has no subtypes.
34const VOIDS_ELEMENT: &str = "IFCRELVOIDSELEMENT";
35
36/// One voiding: the relation entity and the opening it names.
37#[cfg_attr(not(feature = "lowering"), allow(dead_code))]
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
39pub(crate) struct Voiding {
40 /// The `IfcRelVoidsElement` that authored the subtraction.
41 pub(crate) relation: EntityId,
42 /// The opening removed from the host.
43 pub(crate) opening: EntityId,
44}
45
46/// An opening that the file makes void a second host.
47///
48/// `IfcFeatureElementSubtraction.VoidsElements` is a single-valued inverse
49/// (`IfcRelVoidsElement FOR RelatedOpeningElement`) in IFC2X3 and IFC4: an
50/// opening voids exactly one element. When a file names two hosts, the
51/// relation with the lower id wins. The opening is subtracted from that host
52/// only, never cut into an element it does not belong to, and the rejected
53/// claim is reported here.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
55#[non_exhaustive]
56pub struct VoidingConflict {
57 /// The opening with two hosts.
58 pub opening: EntityId,
59 /// The host it voids.
60 pub kept_host: EntityId,
61 /// The host it is not subtracted from.
62 pub rejected_host: EntityId,
63 /// The `IfcRelVoidsElement` that was rejected.
64 pub relation: EntityId,
65}
66
67/// Every well-formed `(relation, host, opening)` triple, ascending by relation.
68fn all_voidings(model: &Model) -> Vec<(EntityId, EntityId, EntityId)> {
69 let mut all: Vec<_> = model
70 .of_type(VOIDS_ELEMENT)
71 .filter_map(|(relation, entity)| {
72 let slots = Slots::new(relation, entity);
73 let host = ref_at(slots.opt(slot::RELATING_BUILDING_ELEMENT))?;
74 let opening = ref_at(slots.opt(slot::RELATED_OPENING_ELEMENT))?;
75 Some((relation, host, opening))
76 })
77 .collect();
78 all.sort_unstable();
79 all
80}
81
82/// Each opening's host: the one its lowest-id relation names.
83fn kept_hosts(all: &[(EntityId, EntityId, EntityId)]) -> BTreeMap<EntityId, EntityId> {
84 let mut kept = BTreeMap::new();
85 for &(_, host, opening) in all {
86 kept.entry(opening).or_insert(host);
87 }
88 kept
89}
90
91/// Openings the file makes void more than one host, in relation order.
92///
93/// Empty for a conformant file. Kernel-free, like [`openings_of`]. A second
94/// relation naming the same host again is redundant, not a conflict.
95#[must_use]
96pub fn voiding_conflicts(model: &Model) -> Vec<VoidingConflict> {
97 let all = all_voidings(model);
98 let kept = kept_hosts(&all);
99 all.iter()
100 .filter(|(_, host, opening)| kept[opening] != *host)
101 .map(|&(relation, host, opening)| VoidingConflict {
102 opening,
103 kept_host: kept[&opening],
104 rejected_host: host,
105 relation,
106 })
107 .collect()
108}
109
110/// Every voiding of `host`, one per opening, in ascending opening id.
111///
112/// Ordered by the opening rather than by the relation so the result is a
113/// property of the model's content, not of which relation an exporter wrote
114/// first. An opening named by two relations for the same host is subtracted
115/// once: removing the same body twice changes nothing but the cost. An
116/// opening another relation assigns to a different host first is not this
117/// host's (see [`voiding_conflicts`]).
118///
119/// Scans the relations on each call. That is O(relations), which is a few
120/// hundred on a real building; a caller netting every product of a large model
121/// pays O(products x relations), still well under the boolean cost.
122pub(crate) fn voidings_of(model: &Model, host: EntityId) -> Vec<Voiding> {
123 let all = all_voidings(model);
124 let kept = kept_hosts(&all);
125 let mut found: Vec<Voiding> = all
126 .into_iter()
127 .filter(|(_, relating, opening)| *relating == host && kept[opening] == host)
128 .map(|(relation, _, opening)| Voiding { relation, opening })
129 .collect();
130 found.sort_by_key(|voiding| (voiding.opening, voiding.relation));
131 found.dedup_by_key(|voiding| voiding.opening);
132 found
133}
134
135/// The openings voiding `host`, in ascending id.
136///
137/// Kernel-free: answering which openings a wall has is a slot read, so this
138/// works under `--no-default-features` exactly as with the kernel linked. A
139/// relation with a missing or non-reference end is skipped rather than
140/// guessed at; it names no opening this function could return. An opening a
141/// malformed file assigns to two hosts belongs to the first by relation id;
142/// [`voiding_conflicts`] reports the other.
143///
144/// ```
145/// use ifc_geometry::openings_of;
146/// use ifc_model::{EntityId, Model};
147///
148/// // A model with no relations has no openings anywhere.
149/// assert!(openings_of(&Model::new(), EntityId(1)).is_empty());
150/// ```
151pub fn openings_of(model: &Model, host: EntityId) -> Vec<EntityId> {
152 voidings_of(model, host)
153 .into_iter()
154 .map(|voiding| voiding.opening)
155 .collect()
156}
157
158fn ref_at(value: Option<&Value>) -> Option<EntityId> {
159 match value? {
160 Value::Ref(id) => Some(*id),
161 _ => None,
162 }
163}