Skip to main content

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}