ifc_geometry/input/representation.rs
1//! Context and representation-selection views.
2//!
3//! # Why selection is a policy, not a first entry
4//!
5//! A product commonly carries several representations: an Axis
6//! centreline, a FootPrint outline and a Body solid, in file
7//! order. Taking Representations[0] yields a 2D curve for any
8//! wall authored by Revit, which renders as nothing. Callers
9//! that want a solid must ask for one by identifier.
10
11use ifc_model::{Entity, EntityId, Model};
12
13use crate::error::{GeometryError, GeometryResult};
14use crate::slots::Slots;
15
16/// Absolute slots on IfcProductRepresentation.
17pub mod product_shape_slot {
18 /// LIST of IfcRepresentation.
19 pub const REPRESENTATIONS: usize = 2;
20}
21
22/// Absolute slots on IfcRepresentation.
23pub mod representation_slot {
24 /// The IfcRepresentationContext this representation is authored into.
25 pub const CONTEXT_OF_ITEMS: usize = 0;
26 /// Body, Axis, FootPrint; OPTIONAL in the schema.
27 pub const REPRESENTATION_IDENTIFIER: usize = 1;
28 /// Plan, Curve2D, SweptSolid, Brep; OPTIONAL in the schema.
29 pub const REPRESENTATION_TYPE: usize = 2;
30 /// The representation items themselves.
31 pub const ITEMS: usize = 3;
32}
33
34/// One IfcRepresentation: a named set of items in a context.
35#[derive(Debug, Clone, Copy)]
36pub struct Representation<'m> {
37 slots: Slots<'m>,
38}
39
40impl<'m> Representation<'m> {
41 /// Wrap an entity assumed to be an IfcRepresentation subtype.
42 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
43 Self {
44 slots: Slots::new(id, entity),
45 }
46 }
47
48 /// Body, Axis, FootPrint; absent when the author omitted it.
49 pub fn identifier(&self) -> Option<String> {
50 self.slots
51 .opt_text(representation_slot::REPRESENTATION_IDENTIFIER)
52 }
53
54 /// Plan, Curve2D, SweptSolid, Brep; absent when the author omitted it.
55 pub fn representation_type(&self) -> Option<String> {
56 self.slots
57 .opt_text(representation_slot::REPRESENTATION_TYPE)
58 }
59
60 /// The `IfcRepresentationContext` this representation is authored into.
61 pub fn context(&self) -> Option<EntityId> {
62 match self.slots.opt(representation_slot::CONTEXT_OF_ITEMS)? {
63 ifc_model::Value::Ref(id) => Some(*id),
64 _ => None,
65 }
66 }
67
68 /// The representation items to lower.
69 pub fn items(&self) -> GeometryResult<Vec<EntityId>> {
70 self.slots.req_ref_list(representation_slot::ITEMS, "Items")
71 }
72}
73
74/// One IfcProductRepresentation: the ordered representations of a product.
75#[derive(Debug, Clone, Copy)]
76pub struct ProductShape<'m> {
77 slots: Slots<'m>,
78}
79
80impl<'m> ProductShape<'m> {
81 /// Wrap an entity assumed to be an IfcProductRepresentation subtype.
82 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
83 Self {
84 slots: Slots::new(id, entity),
85 }
86 }
87
88 /// Representations in authored order.
89 pub fn representations(&self) -> GeometryResult<Vec<EntityId>> {
90 self.slots
91 .req_ref_list(product_shape_slot::REPRESENTATIONS, "Representations")
92 }
93}
94
95/// Representation identifiers that carry solid geometry, best first.
96///
97/// Body is the shape a viewer draws. Facetation is the IFC2x3-era
98/// fallback some exporters still emit. Axis and FootPrint are
99/// deliberately absent: they are 2D annotations, and selecting one
100/// silently replaces a solid with a line.
101pub const SOLID_IDENTIFIERS: &[&str] = &["Body", "Facetation"];
102
103/// Pick the representation a viewer should draw for this product.
104///
105/// Preference order is SOLID_IDENTIFIERS, then any representation whose
106/// identifier is missing. A file whose only representation is an Axis
107/// returns None rather than a curve masquerading as a body.
108pub fn select_shape_representation(
109 model: &Model,
110 product: EntityId,
111) -> GeometryResult<Option<EntityId>> {
112 let entity = model.get(product).ok_or(GeometryError::MissingEntity {
113 referrer: product,
114 missing: product,
115 })?;
116 let Some(shape_id) = super::product::Product::new(product, entity).representation() else {
117 return Ok(None);
118 };
119
120 let shape_entity = model.get(shape_id).ok_or(GeometryError::MissingEntity {
121 referrer: product,
122 missing: shape_id,
123 })?;
124 let candidates = ProductShape::new(shape_id, shape_entity).representations()?;
125
126 for wanted in SOLID_IDENTIFIERS {
127 for &candidate in &candidates {
128 let Some(entity) = model.get(candidate) else {
129 continue;
130 };
131 let identifier = Representation::new(candidate, entity).identifier();
132 if identifier.as_deref() == Some(*wanted) {
133 return Ok(Some(candidate));
134 }
135 }
136 }
137
138 // No named solid representation: accept an unnamed one, since some
139 // authors omit the identifier entirely, but never an Axis/FootPrint.
140 for &candidate in &candidates {
141 let Some(entity) = model.get(candidate) else {
142 continue;
143 };
144 if Representation::new(candidate, entity)
145 .identifier()
146 .is_none()
147 {
148 return Ok(Some(candidate));
149 }
150 }
151 Ok(None)
152}
153
154/// Representation identifiers that carry 2D drawing geometry, best first.
155///
156/// The inverse of [`SOLID_IDENTIFIERS`]. `Plan` and `Annotation` are authored
157/// for drawings directly; `FootPrint` is the product outline a plan is usually
158/// built from; `Axis` is the centreline, useful but least specific.
159///
160/// `Body` is deliberately absent: a solid in a plan selector would have to be
161/// sectioned before it means anything in 2D, and returning one would let a
162/// caller draw a projected solid where a plan curve was expected.
163pub const PLAN_IDENTIFIERS: &[&str] = &["Plan", "Annotation", "FootPrint", "Axis"];
164
165/// Caller intent for representation selection.
166#[non_exhaustive]
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
168pub enum RepresentationPurpose {
169 /// Product shape suitable for model/body consumers.
170 Body,
171 /// Plan/annotation/axis geometry suitable for 2D drawing consumers.
172 Plan,
173}
174
175/// Select a product representation using an explicit consumer purpose.
176pub fn select_product_representation(
177 model: &Model,
178 product: EntityId,
179 purpose: RepresentationPurpose,
180) -> GeometryResult<Option<EntityId>> {
181 match purpose {
182 RepresentationPurpose::Body => select_shape_representation(model, product),
183 RepresentationPurpose::Plan => select_plan_representation(model, product),
184 }
185}
186
187/// Pick the representation a 2D drawing should use for this product.
188///
189/// Preference is, in order:
190///
191/// 1. a [`PLAN_IDENTIFIERS`] match **inside** a `PLAN_VIEW` sub-context --
192/// drawable geometry the author explicitly targeted at a plan;
193/// 2. otherwise the best [`PLAN_IDENTIFIERS`] match in any context.
194///
195/// The two rules are intersected, not ordered. Treating the context as
196/// sufficient on its own looks reasonable -- an author who sets
197/// `TargetView = .PLAN_VIEW.` has stated intent -- but ArchiCAD authors
198/// `Box`/`BoundingBox` shape representations *inside* a `PLAN_VIEW`
199/// sub-context. A context-first rule returns those boxes and never reaches
200/// the identifier list: on `AC20-FZK-Haus.ifc` that was 107 of 253 shape
201/// representations, and every plan lookup came back a box. Authorial intent
202/// selects *between* drawable candidates; it does not make a bounding box
203/// drawable.
204///
205/// Returns `None` when the product has only solid or bounding-box geometry.
206/// That is a real answer, not a failure: deriving a plan from a solid needs
207/// sectioning, which this crate does not do. A caller that wants an outline
208/// anyway should say so explicitly rather than be handed a box that claims
209/// to be a plan.
210pub fn select_plan_representation(
211 model: &Model,
212 product: EntityId,
213) -> GeometryResult<Option<EntityId>> {
214 let entity = model.get(product).ok_or(GeometryError::MissingEntity {
215 referrer: product,
216 missing: product,
217 })?;
218 let Some(shape_id) = super::product::Product::new(product, entity).representation() else {
219 return Ok(None);
220 };
221 let shape_entity = model.get(shape_id).ok_or(GeometryError::MissingEntity {
222 referrer: product,
223 missing: shape_id,
224 })?;
225 let candidates = ProductShape::new(shape_id, shape_entity).representations()?;
226
227 // 1. Drawable AND explicitly targeted at a plan.
228 for wanted in PLAN_IDENTIFIERS {
229 for &candidate in &candidates {
230 let Some(entity) = model.get(candidate) else {
231 continue;
232 };
233 if Representation::new(candidate, entity)
234 .identifier()
235 .as_deref()
236 != Some(*wanted)
237 {
238 continue;
239 }
240 let Some(context) = super::context::context_of(model, candidate) else {
241 continue;
242 };
243 if context.is_plan_view() {
244 return Ok(Some(candidate));
245 }
246 }
247 }
248
249 // 2. Drawable in any context.
250
251 for wanted in PLAN_IDENTIFIERS {
252 for &candidate in &candidates {
253 let Some(entity) = model.get(candidate) else {
254 continue;
255 };
256 if Representation::new(candidate, entity)
257 .identifier()
258 .as_deref()
259 == Some(*wanted)
260 {
261 return Ok(Some(candidate));
262 }
263 }
264 }
265 Ok(None)
266}