Skip to main content

step_io/scene/
mesh.rs

1//! Read-only handles over tessellated (display-mesh) geometry.
2//!
3//! AP242 tessellated items carry a pre-triangulated copy of the exact b-rep
4//! for direct display — in some exports they are the *only* real shape. The
5//! [`Mesh`] handle exposes the data a viewer consumes — vertex positions,
6//! expanded triangles and normals — without touching the exact geometry;
7//! [`Mesh::face`] links back to the precise b-rep face where the file
8//! provides it. (This is a visualization mesh, not an analysis/FEM mesh.)
9
10// Every public method here is a pure read accessor (see geometry.rs).
11#![allow(clippy::must_use_candidate)]
12
13use crate::generated::model as m;
14use crate::scene::geometry::{Face, Solid};
15use crate::scene::{Ctx, Scene};
16
17/// Model-wide mesh enumeration.
18impl Scene<'_> {
19    /// Every mesh group in the model (`TESSELLATED_SOLID` and
20    /// `TESSELLATED_SHELL` — one part's bundle of mesh faces, with a link to
21    /// the precise b-rep solid where the file provides one).
22    pub fn all_mesh_groups(&self) -> impl Iterator<Item = MeshGroup<'_>> + '_ {
23        let cx = self.ctx();
24        let solids = (0..cx.model.tessellated_solid_arena.items.len()).map(move |i| MeshGroup {
25            cx,
26            which: GroupImpl::Solid(m::TessellatedSolidId(i)),
27        });
28        let shells = (0..cx.model.tessellated_shell_arena.items.len()).map(move |i| MeshGroup {
29            cx,
30            which: GroupImpl::Shell(m::TessellatedShellId(i)),
31        });
32        solids.chain(shells)
33    }
34
35    /// Every tessellated mesh item in the model (`COMPLEX_TRIANGULATED_FACE`,
36    /// `COMPLEX_TRIANGULATED_SURFACE_SET` and plain `TESSELLATED_FACE`).
37    pub fn all_meshes(&self) -> impl Iterator<Item = Mesh<'_>> + '_ {
38        let cx = self.ctx();
39        let faces = (0..cx.model.complex_triangulated_face_arena.items.len()).map(move |i| Mesh {
40            cx,
41            which: MeshImpl::ComplexFace(m::ComplexTriangulatedFaceId(i)),
42        });
43        let sets =
44            (0..cx.model.complex_triangulated_surface_set_arena.items.len()).map(move |i| Mesh {
45                cx,
46                which: MeshImpl::ComplexSurfaceSet(m::ComplexTriangulatedSurfaceSetId(i)),
47            });
48        let plain = (0..cx.model.tessellated_face_arena.items.len()).map(move |i| Mesh {
49            cx,
50            which: MeshImpl::PlainFace(m::TessellatedFaceId(i)),
51        });
52        faces.chain(sets).chain(plain)
53    }
54}
55
56#[derive(Clone, Copy)]
57enum MeshImpl {
58    ComplexFace(m::ComplexTriangulatedFaceId),
59    ComplexSurfaceSet(m::ComplexTriangulatedSurfaceSetId),
60    PlainFace(m::TessellatedFaceId),
61}
62
63/// One tessellated mesh item: a vertex list plus triangles (expanded from the
64/// stored triangle strips/fans).
65#[derive(Clone, Copy)]
66pub struct Mesh<'m> {
67    cx: Ctx<'m>,
68    which: MeshImpl,
69}
70
71/// The normals of a [`Mesh`], as the file stores them: absent, one normal for
72/// the whole mesh, or one per vertex (parallel to [`Mesh::points`]).
73#[derive(Clone, Debug, PartialEq)]
74pub enum MeshNormals {
75    None,
76    Uniform([f64; 3]),
77    PerVertex(Vec<[f64; 3]>),
78}
79
80/// (`name`, `coordinates`, `normals`, `pnindex`, `strips`, `fans`) — the
81/// shared layout; the plain face has no index/triangle data.
82type MeshParts<'m> = (
83    &'m str,
84    &'m m::CoordinatesListRef,
85    &'m [Vec<f64>],
86    &'m [i64],
87    &'m [Vec<i64>],
88    &'m [Vec<i64>],
89);
90
91fn row3(row: &[f64]) -> [f64; 3] {
92    [
93        row.first().copied().unwrap_or(0.0),
94        row.get(1).copied().unwrap_or(0.0),
95        row.get(2).copied().unwrap_or(0.0),
96    ]
97}
98
99impl<'m> Mesh<'m> {
100    fn parts(&self) -> MeshParts<'m> {
101        match self.which {
102            MeshImpl::ComplexFace(i) => {
103                let f = self.cx.model.complex_triangulated_face_arena.get(i.0);
104                (
105                    &f.name,
106                    &f.coordinates,
107                    &f.normals,
108                    &f.pnindex,
109                    &f.triangle_strips,
110                    &f.triangle_fans,
111                )
112            }
113            MeshImpl::ComplexSurfaceSet(i) => {
114                let s = self
115                    .cx
116                    .model
117                    .complex_triangulated_surface_set_arena
118                    .get(i.0);
119                (
120                    &s.name,
121                    &s.coordinates,
122                    &s.normals,
123                    &s.pnindex,
124                    &s.triangle_strips,
125                    &s.triangle_fans,
126                )
127            }
128            MeshImpl::PlainFace(i) => {
129                let f = self.cx.model.tessellated_face_arena.get(i.0);
130                (&f.name, &f.coordinates, &f.normals, &[], &[], &[])
131            }
132        }
133    }
134
135    pub fn name(&self) -> &'m str {
136        self.parts().0
137    }
138
139    /// This mesh's global identity (a `Copy` key for maps / deduplication).
140    pub fn key(&self) -> m::EntityKey {
141        match self.which {
142            MeshImpl::ComplexFace(i) => m::EntityKey::ComplexTriangulatedFace(i),
143            MeshImpl::ComplexSurfaceSet(i) => m::EntityKey::ComplexTriangulatedSurfaceSet(i),
144            MeshImpl::PlainFace(i) => m::EntityKey::TessellatedFace(i),
145        }
146    }
147
148    /// The vertex positions, `pnindex` resolved: entry `n` is the position of
149    /// the vertex that [`Mesh::triangles`] refers to as `n` (0-based).
150    pub fn points(&self) -> Vec<[f64; 3]> {
151        let (_, coords, _, pnindex, ..) = self.parts();
152        let m::CoordinatesListRef::CoordinatesList(id) = coords;
153        let all = &self
154            .cx
155            .model
156            .coordinates_list_arena
157            .get(id.0)
158            .position_coords;
159        if pnindex.is_empty() {
160            all.iter().map(|row| row3(row)).collect()
161        } else {
162            pnindex
163                .iter()
164                .filter_map(|&i| {
165                    let idx = usize::try_from(i).ok()?.checked_sub(1)?;
166                    all.get(idx).map(|row| row3(row))
167                })
168                .collect()
169        }
170    }
171
172    /// The triangles as 0-based indices into [`Mesh::points`], expanded from
173    /// the stored triangle strips and fans (the same expansion OCCT applies;
174    /// only the emission order differs). Degenerate entries (repeated
175    /// indices) are skipped per the encoding; an out-of-range index skips the
176    /// triangle with a warning. A plain `TESSELLATED_FACE` stores no
177    /// triangles, so its list is empty.
178    pub fn triangles(&self) -> Vec<[usize; 3]> {
179        let (_, _, _, _, strips, fans) = self.parts();
180        let n = self.points().len();
181        let cx = self.cx;
182        let mut out = Vec::new();
183        let mut push = |a: i64, b: i64, c: i64| {
184            let tri: Option<Vec<usize>> = [a, b, c]
185                .iter()
186                .map(|&v| {
187                    usize::try_from(v)
188                        .ok()
189                        .and_then(|v| v.checked_sub(1))
190                        .filter(|&v| v < n)
191                })
192                .collect();
193            if let Some(t) = tri {
194                out.push([t[0], t[1], t[2]]);
195            } else {
196                cx.warn("MESH.triangles: vertex index out of range".to_owned());
197            }
198        };
199        for strip in strips {
200            for i in 2..strip.len() {
201                let (a, b, c) = (strip[i - 2], strip[i - 1], strip[i]);
202                if c == a || c == b {
203                    continue; // a degenerate restart entry
204                }
205                // Strips alternate winding so every triangle faces the same way.
206                if i % 2 == 0 {
207                    push(a, c, b);
208                } else {
209                    push(a, b, c);
210                }
211            }
212        }
213        for fan in fans {
214            for i in 2..fan.len() {
215                let (centre, prev, cur) = (fan[0], fan[i - 1], fan[i]);
216                if cur == fan[i - 2] || prev == fan[i - 2] {
217                    continue; // a degenerate restart entry
218                }
219                push(centre, cur, prev);
220            }
221        }
222        out
223    }
224
225    /// The normals, as stored: none, one for the whole mesh, or one per
226    /// vertex. Any other count is malformed and reported as a warning.
227    pub fn normals(&self) -> MeshNormals {
228        let (_, _, normals, ..) = self.parts();
229        match normals.len() {
230            0 => MeshNormals::None,
231            1 => MeshNormals::Uniform(row3(&normals[0])),
232            k if k == self.points().len() => {
233                MeshNormals::PerVertex(normals.iter().map(|row| row3(row)).collect())
234            }
235            _ => {
236                self.cx
237                    .warn("MESH.normals: count is neither 1 nor per-vertex".to_owned());
238                MeshNormals::None
239            }
240        }
241    }
242
243    /// The precise b-rep face this mesh tessellates, where the file links one
244    /// (`geometric_link`); surface links and surface sets have no face.
245    pub fn face(&self) -> Option<Face<'m>> {
246        let link = match self.which {
247            MeshImpl::ComplexFace(i) => self
248                .cx
249                .model
250                .complex_triangulated_face_arena
251                .get(i.0)
252                .geometric_link
253                .as_ref(),
254            MeshImpl::PlainFace(i) => self
255                .cx
256                .model
257                .tessellated_face_arena
258                .get(i.0)
259                .geometric_link
260                .as_ref(),
261            MeshImpl::ComplexSurfaceSet(_) => None,
262        }?;
263        match link {
264            m::FaceOrSurfaceRef::AdvancedFace(id) => Some(Face::from_advanced_face(self.cx, *id)),
265            _ => None,
266        }
267    }
268}
269
270// ---------------------------------------------------------------------------
271// MeshGroup (TESSELLATED_SOLID / TESSELLATED_SHELL containers)
272// ---------------------------------------------------------------------------
273
274#[derive(Clone, Copy)]
275enum GroupImpl {
276    Solid(m::TessellatedSolidId),
277    Shell(m::TessellatedShellId),
278}
279
280/// One part's bundle of mesh faces — a `TESSELLATED_SOLID` or
281/// `TESSELLATED_SHELL` (parallel containers; a shell is not nested in a
282/// solid). [`MeshGroup::solid`] reaches the precise b-rep part.
283#[derive(Clone, Copy)]
284pub struct MeshGroup<'m> {
285    cx: Ctx<'m>,
286    which: GroupImpl,
287}
288
289impl<'m> MeshGroup<'m> {
290    fn items(&self) -> &'m [m::TessellatedStructuredItemRef] {
291        match self.which {
292            GroupImpl::Solid(i) => &self.cx.model.tessellated_solid_arena.get(i.0).items,
293            GroupImpl::Shell(i) => &self.cx.model.tessellated_shell_arena.get(i.0).items,
294        }
295    }
296
297    pub fn name(&self) -> &'m str {
298        match self.which {
299            GroupImpl::Solid(i) => &self.cx.model.tessellated_solid_arena.get(i.0).name,
300            GroupImpl::Shell(i) => &self.cx.model.tessellated_shell_arena.get(i.0).name,
301        }
302    }
303
304    /// This group's global identity (a `Copy` key for maps / deduplication).
305    pub fn key(&self) -> m::EntityKey {
306        match self.which {
307            GroupImpl::Solid(i) => m::EntityKey::TessellatedSolid(i),
308            GroupImpl::Shell(i) => m::EntityKey::TessellatedShell(i),
309        }
310    }
311
312    /// The mesh faces bundled in this group.
313    pub fn meshes(&self) -> Vec<Mesh<'m>> {
314        let cx = self.cx;
315        self.items()
316            .iter()
317            .filter_map(|it| {
318                let which = match it {
319                    m::TessellatedStructuredItemRef::ComplexTriangulatedFace(id) => {
320                        MeshImpl::ComplexFace(*id)
321                    }
322                    m::TessellatedStructuredItemRef::TessellatedFace(id) => {
323                        MeshImpl::PlainFace(*id)
324                    }
325                    _ => return None,
326                };
327                Some(Mesh { cx, which })
328            })
329            .collect()
330    }
331
332    /// The precise b-rep solid this group tessellates, where the file links
333    /// one: a tessellated solid links it directly (`geometric_link`); a
334    /// tessellated shell links the closed shell (`topological_link`), reached
335    /// back through the solid that owns it.
336    pub fn solid(&self) -> Option<Solid<'m>> {
337        let cx = self.cx;
338        match self.which {
339            GroupImpl::Solid(i) => {
340                let link = cx
341                    .model
342                    .tessellated_solid_arena
343                    .get(i.0)
344                    .geometric_link
345                    .as_ref()?;
346                match link {
347                    m::ManifoldSolidBrepRef::ManifoldSolidBrep(id) => Some(Solid::from_id(cx, *id)),
348                    m::ManifoldSolidBrepRef::BrepWithVoids(id) => {
349                        Some(Solid::from_void_id(cx, *id))
350                    }
351                    m::ManifoldSolidBrepRef::Complex(_) => None,
352                }
353            }
354            GroupImpl::Shell(i) => {
355                let link = cx
356                    .model
357                    .tessellated_shell_arena
358                    .get(i.0)
359                    .topological_link
360                    .as_ref()?;
361                let m::ConnectedFaceSetRef::ClosedShell(shell_id) = link else {
362                    return None;
363                };
364                let rg = cx.ref_graph();
365                let outer_is = |outer: &m::ClosedShellRef| matches!(outer, m::ClosedShellRef::ClosedShell(s) if s == shell_id);
366                for r in rg.referrers(m::EntityKey::ClosedShell(*shell_id)) {
367                    match r {
368                        m::EntityKey::ManifoldSolidBrep(sid) => {
369                            let brep = cx.model.manifold_solid_brep_arena.get(sid.0);
370                            if outer_is(&brep.outer) {
371                                return Some(Solid::from_id(cx, *sid));
372                            }
373                        }
374                        m::EntityKey::BrepWithVoids(sid) => {
375                            let brep = cx.model.brep_with_voids_arena.get(sid.0);
376                            if outer_is(&brep.outer) {
377                                return Some(Solid::from_void_id(cx, *sid));
378                            }
379                        }
380                        _ => {}
381                    }
382                }
383                None
384            }
385        }
386    }
387}
388
389impl<'m> Mesh<'m> {
390    /// The group (part bundle) containing this mesh, if any — the first
391    /// `TESSELLATED_SOLID` / `TESSELLATED_SHELL` whose items include it.
392    pub fn group(&self) -> Option<MeshGroup<'m>> {
393        let cx = self.cx;
394        let rg = cx.ref_graph();
395        for r in rg.referrers(self.key()) {
396            let which = match r {
397                m::EntityKey::TessellatedSolid(id) => GroupImpl::Solid(*id),
398                m::EntityKey::TessellatedShell(id) => GroupImpl::Shell(*id),
399                _ => continue,
400            };
401            return Some(MeshGroup { cx, which });
402        }
403        None
404    }
405}