Skip to main content

energyml_geometry/
geometry.rs

1//! A representation, turned into patches of points and how they connect.
2//!
3//! RESQML splits a representation into **patches**, and the split is not
4//! cosmetic: a triangulated surface has one patch per set of triangles, each
5//! with its own point array and its own indices, and merging them would
6//! renumber everything. So the result keeps them apart and leaves the decision
7//! to whoever draws it.
8//!
9//! # Which representations
10//!
11//! | | |
12//! |---|---|
13//! | `TriangulatedSetRepresentation` | triangles, per patch |
14//! | `PointSetRepresentation` | points, per patch |
15//! | `PolylineRepresentation`, `PolylineSetRepresentation` | polylines |
16//! | `Grid2dRepresentation` | a lattice, usually with z from a file |
17//! | `IjkGridRepresentation` | pillars, split coordinate lines, cells of eight corners |
18//! | `WellboreTrajectoryRepresentation` | the survey stations, joined |
19//! | `Trajectory` (WITSML) | its stations, placed by minimum curvature from its datum |
20//! | `UnstructuredGridRepresentation` | cells of explicit faces, faces of explicit nodes |
21//! | `SubRepresentation` | elements picked out of another representation |
22//!
23//! A grid with no geometry of its own is not in that list and is not missing
24//! from it: a **local grid** derives its points from its parent's window, which
25//! is [`crate::parent`]'s business, reached through the IJK row above.
26//!
27//! The types are recognised by [`energyml_core::reflect::TypeInfo::type_key`],
28//! which is the same across versions: one branch reads RESQML 2.0.1 and
29//! RESQML 2.2 without knowing which it has.
30
31use energyml_core::value::{FieldValue, Reflect};
32
33use crate::error::{Error, Result};
34use crate::points::{self, Point};
35use crate::readers::{Readers, RepresentationReader};
36use crate::source::ValueSource;
37use crate::values;
38
39/// How the points of a patch connect.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum Topology {
42    /// Points that mean nothing together.
43    Points,
44    /// Triples of indices into the patch's points.
45    Triangles(Vec<[u32; 3]>),
46    /// Quadrilaterals: four indices into the patch's points, in ring order.
47    ///
48    /// What a 2D grid's cells are, and what a hexahedron's faces are. Kept as
49    /// four corners rather than split into two triangles, because that is what
50    /// the file says a cell *is* - OBJ, VTK and GeoJSON each write a
51    /// quadrilateral directly, and halving it here would hand all three a
52    /// decision none of them asked for.
53    ///
54    /// The ring is oriented so its signed area in the XY plane is positive -
55    /// **measured, not assumed**. That is the winding RFC 7946 asks of a
56    /// GeoJSON exterior ring, and the one a renderer reads as front-facing.
57    Quads(Vec<[u32; 4]>),
58    /// Rings of any length: one entry per ring, each a list of point indices.
59    ///
60    /// The general case [`Topology::Quads`] is the cheap special case of. Both
61    /// exist on purpose: a grid produces hundreds of thousands of cells and a
62    /// `Vec<u32>` each would be that many allocations, while an outline
63    /// produces one ring of arbitrary length that no fixed array can hold.
64    ///
65    /// Wound like a quadrilateral - positive signed area in XY, measured - and
66    /// **not closed**: the repeat of the first point is a GeoJSON requirement,
67    /// added where GeoJSON is written rather than carried everywhere.
68    Polygons(Vec<Vec<u32>>),
69    /// One or more runs of consecutive points; each entry is a run's length.
70    ///
71    /// Closed polylines are not marked here - RESQML says so in a separate
72    /// boolean array, and inventing a convention would be worse than leaving
73    /// it out.
74    Polylines(Vec<u32>),
75    /// Cells of eight corners, indexed into the patch's points.
76    ///
77    /// The corners are in VTK's order - face `(0,1,2,3)`'s right-hand normal
78    /// points at face `(4,5,6,7)` - because that is the one convention every
79    /// consumer states explicitly, and a hexahedron built the other way round
80    /// renders as a hole.
81    Hexahedra {
82        /// One entry per cell, eight point indices each.
83        cells: Vec<[u32; 8]>,
84        /// `Ni`, `Nj`, `Nk` - the shape the cells were built from.
85        dimensions: [usize; 3],
86        /// **The RESQML cell index of each entry**, or empty when they coincide.
87        ///
88        /// A position here is *not* always the number RESQML calls the cell.
89        /// The reader drops a cell its `CellGeometryIsDefined` flag marks
90        /// undefined, and one whose pillars carry no location - so `cells` is
91        /// **compacted**, and on a grid with holes every entry after the first
92        /// hole sits one place before the cell it belongs to.
93        ///
94        /// It matters to anything that addresses a cell **by the number the
95        /// file uses**: a `GridConnectionSetRepresentation` names cell pairs
96        /// that way, and a reader that indexed straight into `cells` would be
97        /// right on an unbroken grid and quietly wrong on exactly the faulted
98        /// and eroded ones a connection set exists for. PaleoGTM has 3 647
99        /// undefined cells out of 33 048.
100        ///
101        /// **Empty means the identity** - nothing was dropped, so a position is
102        /// the index - which is the ordinary case and costs no memory on a grid
103        /// of ten million cells. Read it through [`Topology::cell_at`] rather
104        /// than directly, so that convention lives in one place.
105        index: Vec<u32>,
106    },
107    /// A regular grid, `fastest` × `slowest` points, row by row.
108    Grid {
109        /// Points along the fastest-varying axis.
110        fastest: usize,
111        /// Points along the slowest-varying axis.
112        slowest: usize,
113    },
114    /// Cells bounded by faces of any shape: an unstructured grid.
115    Polyhedra(Polyhedra),
116    /// Indexed segments: pairs of node indices, each pair one edge.
117    ///
118    /// What [`Topology::Polylines`] cannot say - that topology addresses
119    /// **consecutive** points, and the edges of a wireframe join nodes that
120    /// are nowhere near each other in the array. This is the variant its
121    /// module doc promised (*"adding `Topology::Segments` later would make it
122    /// work with no change here"*), and what `Shape::Lines` on a surface now
123    /// produces: the **cell edges**, each stored once. Derived data, like the
124    /// skin of a volume - not an invention.
125    Segments(Vec<[u32; 2]>),
126}
127
128impl Topology {
129    /// How many **cells** this topology holds - one per triangle, quad, ring,
130    /// polyline or volume cell, which is what a per-cell property has one
131    /// value for. The one definition; [`Geometry::cell_count`] and the
132    /// triangle→cell numbering of [`Geometry::drawable`] both sum it, and two
133    /// copies of this match is how they would drift apart.
134    pub fn cell_count(&self) -> usize {
135        match self {
136            Topology::Triangles(triangles) => triangles.len(),
137            Topology::Quads(quads) => quads.len(),
138            Topology::Polygons(rings) => rings.len(),
139            Topology::Polylines(runs) => runs.len(),
140            Topology::Hexahedra { cells, .. } => cells.len(),
141            Topology::Grid { fastest, slowest } => fastest.saturating_sub(1) * slowest.saturating_sub(1),
142            Topology::Polyhedra(cells) => cells.cell_count(),
143            // A segment is a wireframe's cell - which is also what drops
144            // per-cell attributes through the conversion: the count changes,
145            // and a surface-cell value has no honest place on an edge shared
146            // by two cells.
147            Topology::Segments(pairs) => pairs.len(),
148            Topology::Points => 0,
149        }
150    }
151}
152
153/// Cells bounded by faces, each face a ring of nodes.
154///
155/// The shape an unstructured grid has and a hexahedron does not. A cell is not
156/// eight corners in a known order; it is a list of **faces**, each face a list
157/// of **nodes**, and one face is shared by the two cells on either side of it.
158/// That last part is why the orientation cannot live on the face: the stored
159/// winding turns outward for one of the two cells and inward for the other, so
160/// the flag belongs to the *pair*.
161///
162/// Stored flat - four runs and a parallel flag array - because a corpus grid is
163/// two cells and a real one is millions, and three levels of `Vec` for the
164/// latter is a lot of allocations to describe a list of integers.
165#[derive(Debug, Clone, PartialEq, Eq, Default)]
166pub struct Polyhedra {
167    /// Nodes of every face, concatenated.
168    face_nodes: Vec<u32>,
169    /// Where each face begins in `face_nodes`, with a final entry holding the
170    /// total: face `f` is `face_nodes[face_starts[f]..face_starts[f + 1]]`.
171    face_starts: Vec<u32>,
172    /// Faces of every cell, concatenated.
173    cell_faces: Vec<u32>,
174    /// Where each cell begins in `cell_faces`, same convention.
175    cell_starts: Vec<u32>,
176    /// One flag per entry of `cell_faces`: whether that face's stored winding
177    /// already turns its right-hand normal **out of that cell**.
178    outward: Vec<bool>,
179}
180
181impl Polyhedra {
182    /// Build from the four runs, with the flags parallel to `cell_faces`.
183    ///
184    /// `face_starts` and `cell_starts` are exclusive running totals *with* a
185    /// leading zero and a trailing total, so both have one more entry than the
186    /// thing they index. RESQML's own `CumulativeLength` has neither; the
187    /// conversion happens where the file is read, once.
188    pub fn new(
189        face_nodes: Vec<u32>,
190        face_starts: Vec<u32>,
191        cell_faces: Vec<u32>,
192        cell_starts: Vec<u32>,
193        outward: Vec<bool>,
194    ) -> Self {
195        Self {
196            face_nodes,
197            face_starts,
198            cell_faces,
199            cell_starts,
200            outward,
201        }
202    }
203
204    /// How many cells.
205    pub fn cell_count(&self) -> usize {
206        self.cell_starts.len().saturating_sub(1)
207    }
208
209    /// How many distinct faces - not the number of cell-face pairs, which is
210    /// larger wherever a face is shared.
211    pub fn face_count(&self) -> usize {
212        self.face_starts.len().saturating_sub(1)
213    }
214
215    /// The nodes of one face, in the order the file stored them.
216    pub fn face(&self, face: usize) -> &[u32] {
217        let (from, to) = (
218            self.face_starts[face] as usize,
219            self.face_starts[face + 1] as usize,
220        );
221        &self.face_nodes[from..to]
222    }
223
224    /// The faces of one cell, each with whether its stored winding points out
225    /// of *this* cell.
226    pub fn cell(&self, cell: usize) -> impl Iterator<Item = (usize, bool)> + '_ {
227        let (from, to) = (
228            self.cell_starts[cell] as usize,
229            self.cell_starts[cell + 1] as usize,
230        );
231        (from..to).map(move |slot| (self.cell_faces[slot] as usize, self.outward[slot]))
232    }
233
234    /// The nodes of one cell's faces, each already wound so the right-hand
235    /// normal points **out** of the cell.
236    ///
237    /// What every consumer wants and none of them says twice: a face written
238    /// for the neighbour comes back reversed here, so a caller can hand the
239    /// result straight to a renderer without carrying the flag around.
240    pub fn faces_outward(&self, cell: usize) -> impl Iterator<Item = Vec<u32>> + '_ {
241        self.cell(cell).map(move |(face, outward)| {
242            let nodes = self.face(face);
243            if outward {
244                nodes.to_vec()
245            } else {
246                nodes.iter().rev().copied().collect()
247            }
248        })
249    }
250
251    /// Every cell's boundary as outward-facing triangles.
252    ///
253    /// A fan from each face's first node. Exact for a triangle, exact for a
254    /// planar polygon, and an approximation for a face whose corners are not
255    /// coplanar - which the schema neither forbids nor says how to split, the
256    /// one place it comes close being the optional hinge-node faces. The fan is
257    /// stated rather than hidden: a caller wanting the hinge-node tessellation
258    /// has to do it from the face lists, which are right here.
259    pub fn triangles(&self) -> Vec<[u32; 3]> {
260        let mut out = Vec::new();
261        for cell in 0..self.cell_count() {
262            for nodes in self.faces_outward(cell) {
263                for corner in 1..nodes.len().saturating_sub(1) {
264                    out.push([nodes[0], nodes[corner], nodes[corner + 1]]);
265                }
266            }
267        }
268        out
269    }
270
271    /// The grid's **outside**: the faces belonging to one cell only, each wound
272    /// so its right-hand normal points out of the grid.
273    ///
274    /// # Why the boundary and not every face
275    ///
276    /// [`Polyhedra::triangles`] emits every cell's every face, interior ones
277    /// included - right for counting, wrong for drawing. A grid of a hundred
278    /// thousand cells has a few thousand faces on its skin and hundreds of
279    /// thousands inside it, each shared by two cells and each drawn twice, one
280    /// of the two facing the camera. What comes out is an opaque block whose
281    /// surface z-fights with itself, at ten times the cost of the picture
282    /// actually wanted.
283    ///
284    /// A face is on the boundary when exactly one cell claims it - the same
285    /// rule the hexahedral path uses, and here it needs no geometry at all,
286    /// because the shared faces are already shared *by index*: the file states
287    /// which cells meet at which face, so counting the claims is exact where
288    /// hashing corner sets is a reconstruction.
289    ///
290    /// The winding comes from the flag the file carries for that cell-face
291    /// pair, so the ring returned turns outward for the one cell that owns it -
292    /// which, for a boundary face, is outward from the grid.
293    ///
294    /// Rings of any length, not triangles: a face may be a quadrilateral, and
295    /// splitting it here would hand a consumer that wants polygons - GeoJSON,
296    /// VTK, OBJ - a decision it did not ask for. [`Geometry::drawable`] fans
297    /// them where a renderer needs triangles.
298    pub fn boundary(&self) -> Vec<Vec<u32>> {
299        let mut claims = vec![0u32; self.face_count()];
300        for cell in 0..self.cell_count() {
301            for (face, _) in self.cell(cell) {
302                if let Some(count) = claims.get_mut(face) {
303                    *count += 1;
304                }
305            }
306        }
307        let mut out = Vec::new();
308        for cell in 0..self.cell_count() {
309            for (face, outward) in self.cell(cell) {
310                if claims.get(face).copied() != Some(1) {
311                    continue;
312                }
313                let nodes = self.face(face);
314                out.push(if outward {
315                    nodes.to_vec()
316                } else {
317                    nodes.iter().rev().copied().collect()
318                });
319            }
320        }
321        out
322    }
323
324    /// How many cell-face pairs there are, summed over the cells.
325    ///
326    /// The length `CellFaceIsRightHanded` has to be, and larger than
327    /// [`Polyhedra::face_count`] by one per shared face.
328    pub fn slot_count(&self) -> usize {
329        self.cell_faces.len()
330    }
331}
332
333/// One patch: its points, and what they form.
334#[derive(Debug, Clone, PartialEq)]
335pub struct Patch {
336    /// Index of the patch inside the representation, as RESQML numbers them.
337    pub index: usize,
338    /// Where this patch's points start in the representation's own node
339    /// numbering - the running sum of the preceding patches' point counts.
340    ///
341    /// A subrepresentation, a property, or anything else that indexes *nodes of
342    /// the representation* counts from here; [`Topology`] indices do not, see
343    /// [`Patch::topology`].
344    pub node_offset: usize,
345    /// The points, in the representation's local coordinate system.
346    pub points: Vec<Point>,
347    /// How they connect - always indexed into this patch's own [`points`],
348    /// whichever convention the file used.
349    ///
350    /// [`points`]: Patch::points
351    pub topology: Topology,
352    /// Which of this patch's polylines close back on their first node.
353    ///
354    /// One entry per run of [`Topology::Polylines`], **empty when the file
355    /// says nothing** - which is not the same as *all open*: a producer that
356    /// omits `ClosedPolylines` has stated nothing, and a reader that drew the
357    /// closing edge anyway would invent a ring.
358    ///
359    /// It lives here and not in [`Topology::Polylines`] because RESQML states
360    /// it in a boolean array beside the counts, and the topology carries
361    /// lengths. Added after a Volve polyline the file
362    /// calls closed was drawn open: the reader took
363    /// `NodeCountPerPolyline` and never `ClosedPolylines`, so the last segment
364    /// of every ring was missing.
365    pub closed: Vec<bool>,
366}
367
368impl Topology {
369    /// The RESQML cell index of the `at`-th cell of this topology.
370    ///
371    /// The one place that knows what an empty `index` means. `None` when `at`
372    /// is past the end, which a caller walking its own list cannot produce and
373    /// a caller resolving a number out of a file certainly can.
374    pub fn cell_at(&self, at: usize) -> Option<u32> {
375        match self {
376            Topology::Hexahedra { cells, index, .. } => {
377                if at >= cells.len() {
378                    return None;
379                }
380                Some(index.get(at).copied().unwrap_or(at as u32))
381            }
382            _ => (at < self.cell_count()).then_some(at as u32),
383        }
384    }
385
386    /// Where the cell RESQML numbers `cell` sits in this topology, if it is here.
387    ///
388    /// `None` for a cell the reader dropped - undefined, or with no located
389    /// pillar. That is an answer and not a failure: the file names a cell that
390    /// has no shape, and drawing something for it would be drawing a place the
391    /// grid says is not one.
392    pub fn position_of(&self, cell: u32) -> Option<usize> {
393        match self {
394            Topology::Hexahedra { cells, index, .. } => {
395                if index.is_empty() {
396                    return ((cell as usize) < cells.len()).then_some(cell as usize);
397                }
398                // Sorted by construction - the reader walks k, then j, then i -
399                // so a binary search rather than a scan over ten million cells.
400                index.binary_search(&cell).ok()
401            }
402            _ => ((cell as usize) < self.cell_count()).then_some(cell as usize),
403        }
404    }
405}
406
407/// Everything a representation says about its shape.
408#[derive(Debug, Clone, PartialEq)]
409pub struct Geometry {
410    /// The representation's own type, for a caller that wants to label it.
411    pub kind: &'static str,
412    /// One entry per patch, in order.
413    pub patches: Vec<Patch>,
414}
415
416impl Geometry {
417    /// Total number of points across every patch.
418    pub fn point_count(&self) -> usize {
419        self.patches.iter().map(|patch| patch.points.len()).sum()
420    }
421
422    /// Whether there is anything to draw.
423    pub fn is_empty(&self) -> bool {
424        self.point_count() == 0
425    }
426
427    /// **The nodes of these ranks, in the order asked** - the representation's
428    /// own numbering, the running sum of its patches' points ([`Patch::node_offset`]).
429    ///
430    /// The numbering a click in the scene returns and `transform`'s `nodes`
431    /// reads. A rank the shape does not hold is refused by name: a node taken
432    /// from past the end would be another shape's, or nothing.
433    pub fn nodes_at(&self, ranks: &[usize]) -> std::result::Result<Vec<[f64; 3]>, String> {
434        let all: Vec<&[f64; 3]> = self
435            .patches
436            .iter()
437            .flat_map(|patch| patch.points.iter())
438            .collect();
439        ranks
440            .iter()
441            .map(|&rank| {
442                all.get(rank).map(|p| **p).ok_or_else(|| {
443                    format!(
444                        "node {rank} is named, and the shape holds {} (ranks from 0)",
445                        all.len()
446                    )
447                })
448            })
449            .collect()
450    }
451
452    /// Total number of **cells**, whatever kind of cell this shape has.
453    ///
454    /// A triangle, a polyline, a hexahedron and a grid cell all count as one,
455    /// because that is what a per-cell property has one value for - and what a
456    /// consumer will line those values up against. A shape of loose points has
457    /// no cells at all.
458    pub fn cell_count(&self) -> usize {
459        self.patches.iter().map(|patch| patch.topology.cell_count()).sum()
460    }
461
462    /// Every patch's points in one array, with the triangles renumbered into it.
463    ///
464    /// One vertex buffer and one index buffer is what a renderer actually
465    /// uploads, and doing the renumbering here is the point: it is the one
466    /// place [`Patch::node_offset`] has to be applied, and applying it wrong
467    /// draws a mesh instead of failing.
468    ///
469    /// Patches that are not triangulated contribute their points and no
470    /// indices. That includes both volumetric kinds: a cell is not a surface,
471    /// and the exporters draw its faces themselves rather than take a
472    /// triangulation decided here. [`Polyhedra::triangles`] is there for a
473    /// caller that does want one.
474    pub fn merged(&self) -> (Vec<Point>, Vec<[u32; 3]>) {
475        let mut points = Vec::with_capacity(self.point_count());
476        let mut triangles = Vec::new();
477        for patch in &self.patches {
478            let base = points.len() as u32;
479            points.extend_from_slice(&patch.points);
480            if let Topology::Triangles(patch_triangles) = &patch.topology {
481                triangles.extend(
482                    patch_triangles
483                        .iter()
484                        .map(|t| [t[0] + base, t[1] + base, t[2] + base]),
485                );
486            }
487        }
488        (points, triangles)
489    }
490
491    /// Points, triangles **and line segments**, ready for a graphics API.
492    ///
493    /// # Why this exists next to [`Geometry::merged`]
494    ///
495    /// `merged` emits indices only for [`Topology::Triangles`], which is right
496    /// for what it is for. But a viewer given nothing else draws a **point
497    /// cloud** - and that is what a 2D grid and a well trajectory came out as:
498    /// a horizon rendered as a spray of dots, a borehole as a dotted line. The
499    /// data was there the whole time; nothing turned a cell into two triangles
500    /// or a station list into segments.
501    ///
502    /// So every topology gets an answer here:
503    ///
504    /// - a **grid** becomes two triangles per cell, in the lattice's own
505    ///   row-major order;
506    /// - **quadrilaterals** and **polygons** are fanned from their first
507    ///   corner, which is correct for the convex rings RESQML produces;
508    /// - **polylines** become the segments between consecutive stations;
509    /// - bare **points** stay points, because that is what they are.
510    ///
511    /// A cell with a non-finite corner is **dropped rather than drawn**. A grid
512    /// with holes is ordinary - an eroded horizon, a survey clipped to a
513    /// licence block - and a triangle through `NaN` does not render as a hole,
514    /// it renders as a spike across the whole scene.
515    ///
516    /// And the node itself goes with it. Dropping only the cells leaves the
517    /// `NaN` in the buffer a viewer uploads, which is not a missing dot: three
518    /// numbers that are not numbers make the object's bounding sphere `NaN`, the
519    /// frustum test fails for the whole object, and the framing of the **entire
520    /// scene** goes with it - one console line per frame, about a node that only
521    /// ever meant "no data here". So [`Drawable`] carries locations only, the
522    /// indices are renumbered onto them, and [`Drawable::dropped`] says how many
523    /// there were: a surface that comes out empty because every node was `NaN`
524    /// must not look like a reader that found nothing.
525    pub fn drawable(&self) -> Drawable {
526        let mut points: Vec<Point> = Vec::with_capacity(self.point_count());
527        let mut triangles: Vec<[u32; 3]> = Vec::new();
528        let mut triangle_cells: Vec<u32> = Vec::new();
529        let mut lines: Vec<[u32; 2]> = Vec::new();
530        let mut node_cells: Vec<u32> = Vec::new();
531        // The cell numbering is the **original** topology's, patch by patch -
532        // the numbering `cell_count` sums and a per-cell property lines up
533        // against - even where the drawing swaps in a derived skin.
534        let mut cell_base = 0u32;
535        // One skinned patch and the table is a lie for the whole object: a
536        // boundary face is not a cell, so the mapping is withheld entirely
537        // rather than guessed for part of it.
538        let mut unmappable = false;
539
540        for patch in &self.patches {
541            let base = points.len() as u32;
542            // **A volume's node that no cell names is not a location.** The
543            // IJK reader drops a cell its `CellGeometryIsDefined` flag marks
544            // undefined, and the standard makes that flag override the
545            // numbers - which stay finite in the file, since nothing obliges a
546            // writer to null them. Left in the buffer, such a node was drawn
547            // as a dot exactly where the grid says there is nothing (seen
548            // on ALWYN's IJK grids). A lattice or a point
549            // set keeps its lone nodes: there a node is a sample in its own
550            // right, and the ALWYN horizons are nothing but lone nodes.
551            let placed = orphans_as_holes(&patch.points, &patch.topology);
552            points.extend_from_slice(&placed);
553            // Every node enters with no cell; the polylines arm - the one
554            // place membership is unique - rewrites its own below.
555            node_cells.resize(points.len(), Drawable::NO_CELL);
556            let finite = |index: u32| -> bool {
557                placed
558                    .get(index as usize)
559                    .is_some_and(|p| p.iter().all(|c| c.is_finite()))
560            };
561            let mut triangle = |a: u32, b: u32, c: u32, cell: u32| {
562                if finite(a) && finite(b) && finite(c) {
563                    triangles.push([a + base, b + base, c + base]);
564                    // In lockstep with the push above: a triangle dropped for a
565                    // non-finite corner drops its cell entry too, or every
566                    // later triangle wears its neighbour's value.
567                    triangle_cells.push(cell);
568                }
569            };
570
571            // **A volume is drawn by its skin**, and the skin is derived here
572            // rather than left to the caller. Leaving it out is what made an
573            // IJK grid and an unstructured grid come out as point clouds: the
574            // arms below had nothing to match, so the viewer fell through to
575            // its last resort and drew the nodes. The data was complete the
576            // whole time - nothing turned a cell into faces.
577            //
578            // Through `shape::reshape` rather than a second implementation:
579            // that module already decides which faces are on the outside, and
580            // two derivations of the same boundary is exactly the pair of
581            // readers that agree on a total and disagree on what it means.
582            //
583            // A refusal is kept rather than swallowed - the arms below still
584            // have a volume case, which emits nothing, so a topology whose
585            // boundary this build cannot derive stays a point cloud instead of
586            // becoming silently empty.
587            let skin = match &patch.topology {
588                volume @ (Topology::Hexahedra { .. } | Topology::Polyhedra(_)) => {
589                    crate::shape::reshape(volume, &patch.points, crate::shape::Shape::Surface).ok()
590                }
591                _ => None,
592            };
593
594            if skin.is_some() {
595                unmappable = true;
596            }
597            match skin.as_ref().unwrap_or(&patch.topology) {
598                Topology::Triangles(found) => {
599                    for (at, t) in found.iter().enumerate() {
600                        triangle(t[0], t[1], t[2], cell_base + at as u32);
601                    }
602                }
603                Topology::Quads(found) => {
604                    for (at, q) in found.iter().enumerate() {
605                        triangle(q[0], q[1], q[2], cell_base + at as u32);
606                        triangle(q[0], q[2], q[3], cell_base + at as u32);
607                    }
608                }
609                Topology::Polygons(rings) => {
610                    for (cell, ring) in rings.iter().enumerate() {
611                        for at in 1..ring.len().saturating_sub(1) {
612                            triangle(ring[0], ring[at], ring[at + 1], cell_base + cell as u32);
613                        }
614                    }
615                }
616                Topology::Grid { fastest, slowest } => {
617                    // Row-major, the fast axis first - the order the lattice
618                    // evaluates its points in, so a cell is four neighbours and
619                    // not an index computation anybody has to re-derive.
620                    for row in 0..slowest.saturating_sub(1) {
621                        for column in 0..fastest.saturating_sub(1) {
622                            let a = (row * fastest + column) as u32;
623                            let b = a + 1;
624                            let c = a + *fastest as u32;
625                            let d = c + 1;
626                            let cell = cell_base + (row * (fastest - 1) + column) as u32;
627                            triangle(a, b, d, cell);
628                            triangle(a, d, c, cell);
629                        }
630                    }
631                }
632                Topology::Polylines(runs) => {
633                    let mut at: u32 = 0;
634                    for (cell, run) in runs.iter().enumerate() {
635                        // Every node of a run belongs to that run and no
636                        // other - the membership that makes per-cell painting
637                        // of lines a lookup instead of a de-index. `get_mut`
638                        // rather than an index: a run declaring more nodes
639                        // than the patch holds is a malformed file, not a
640                        // panic.
641                        for step in 0..*run {
642                            if let Some(entry) = node_cells.get_mut((base + at + step) as usize) {
643                                *entry = cell_base + cell as u32;
644                            }
645                        }
646                        for step in 0..run.saturating_sub(1) {
647                            let from = at + step;
648                            if finite(from) && finite(from + 1) {
649                                lines.push([from + base, from + 1 + base]);
650                            }
651                        }
652                        // And the segment that returns to the first node, when
653                        // the file says the line is closed. **The node is not
654                        // repeated**: a RESQML ring counts its nodes once, and
655                        // adding one would shift every property indexed on
656                        // them.
657                        if patch.closed.get(cell).copied().unwrap_or(false)
658                            && *run >= 3
659                            && finite(at)
660                            && finite(at + run - 1)
661                        {
662                            lines.push([at + run - 1 + base, at + base]);
663                        }
664                        at += run;
665                    }
666                }
667                Topology::Segments(pairs) => {
668                    for pair in pairs {
669                        if finite(pair[0]) && finite(pair[1]) {
670                            lines.push([pair[0] + base, pair[1] + base]);
671                        }
672                    }
673                }
674                // Only reachable when the conversion above refused: the points
675                // stay, and `dropped` is not the place that says so. A volume
676                // this build cannot skin is rare enough to be worth seeing as
677                // the nodes it does have rather than as nothing.
678                Topology::Hexahedra { .. } | Topology::Polyhedra(_) => {}
679                Topology::Points => {}
680            }
681            // The **original** topology advances the numbering, skin or not:
682            // a property counts the volume's cells whether or not this build
683            // can map its boundary back onto them.
684            cell_base += patch.topology.cell_count() as u32;
685        }
686        if unmappable {
687            triangle_cells.clear();
688        }
689        compact(points, triangles, triangle_cells, lines, node_cells)
690    }
691}
692
693/// A volume's points, with every node no cell names turned into a hole.
694///
695/// Borrowed when every node is named - the ordinary grid pays one pass and no
696/// copy - and owned with `NaN` on the orphans otherwise, so that [`compact`]
697/// drops them the way it drops a node the file left null, and counts them in
698/// [`Drawable::dropped`]. Surfaces, lines and point sets are handed back as
699/// they are: a node of theirs that no cell names is still a location.
700pub(crate) fn orphans_as_holes<'a>(
701    points: &'a [Point],
702    topology: &Topology,
703) -> std::borrow::Cow<'a, [Point]> {
704    let named: Vec<u32> = match topology {
705        Topology::Hexahedra { cells, .. } => cells.iter().flatten().copied().collect(),
706        Topology::Polyhedra(cells) => (0..cells.cell_count())
707            .flat_map(|cell| cells.cell(cell).map(|(face, _)| face))
708            .flat_map(|face| cells.face(face).iter().copied())
709            .collect(),
710        _ => return std::borrow::Cow::Borrowed(points),
711    };
712    let mut used = vec![false; points.len()];
713    for index in named {
714        if let Some(slot) = used.get_mut(index as usize) {
715            *slot = true;
716        }
717    }
718    if used.iter().all(|u| *u) {
719        return std::borrow::Cow::Borrowed(points);
720    }
721    let holed: Vec<Point> = points
722        .iter()
723        .zip(&used)
724        .map(|(point, used)| if *used { *point } else { [f64::NAN; 3] })
725        .collect();
726    std::borrow::Cow::Owned(holed)
727}
728
729/// The nodes that are locations, and the indices renumbered onto them.
730///
731/// Cheap where it is usually needed: a shape with nothing to drop is handed
732/// back untouched, so the ordinary case walks the points once and allocates
733/// nothing. Only a shape with holes pays for the map.
734///
735/// An index into a node that was dropped is filtered rather than clamped - by
736/// construction there is none, since a cell with a non-finite corner is never
737/// emitted, but a bent index draws a triangle across the model and there is no
738/// version of that worth having.
739fn compact(
740    points: Vec<Point>,
741    triangles: Vec<[u32; 3]>,
742    triangle_cells: Vec<u32>,
743    lines: Vec<[u32; 2]>,
744    node_cells: Vec<u32>,
745) -> Drawable {
746    let is_location = |point: &Point| point.iter().all(|c| c.is_finite());
747    if points.iter().all(is_location) {
748        return Drawable {
749            points,
750            triangles,
751            lines,
752            dropped: 0,
753            triangle_cells,
754            node_cells,
755        };
756    }
757
758    const GONE: u32 = u32::MAX;
759    let mut moved = Vec::with_capacity(points.len());
760    let mut kept = Vec::with_capacity(points.len());
761    for point in &points {
762        if is_location(point) {
763            moved.push(kept.len() as u32);
764            kept.push(*point);
765        } else {
766            moved.push(GONE);
767        }
768    }
769    let at = |index: u32| moved.get(index as usize).copied().unwrap_or(GONE);
770    // The cell table is filtered **with** its triangle - zipped, not re-derived
771    // - or every triangle after a dropped one wears its neighbour's cell. An
772    // empty table (a volume's skin) stays empty rather than gaining entries.
773    let mapped = !triangle_cells.is_empty();
774    let mut cells_kept = Vec::new();
775    let triangles_kept: Vec<[u32; 3]> = triangles
776        .into_iter()
777        .enumerate()
778        .filter_map(|(index, t)| {
779            let renumbered = [at(t[0]), at(t[1]), at(t[2])];
780            if renumbered.contains(&GONE) {
781                return None;
782            }
783            if mapped && let Some(cell) = triangle_cells.get(index) {
784                cells_kept.push(*cell);
785            }
786            Some(renumbered)
787        })
788        .collect();
789    // The node table follows its nodes: an entry per **kept** point, in the
790    // same order the points were kept - filtered any other way, every node
791    // after a hole would wear its neighbour's cell.
792    let node_cells_kept: Vec<u32> = node_cells
793        .into_iter()
794        .zip(&points)
795        .filter(|(_, point)| is_location(point))
796        .map(|(cell, _)| cell)
797        .collect();
798    Drawable {
799        dropped: points.len() - kept.len(),
800        points: kept,
801        triangles: triangles_kept,
802        triangle_cells: cells_kept,
803        node_cells: node_cells_kept,
804        lines: lines
805            .into_iter()
806            .map(|l| [at(l[0]), at(l[1])])
807            .filter(|l| !l.contains(&GONE))
808            .collect(),
809    }
810}
811
812/// What a viewer needs: the points, and how to join them.
813///
814/// Both index lists at once because a representation can have some of each - a
815/// wellbore frame is stations *and* markers - and a caller that had to ask
816/// twice would walk the patches twice.
817#[derive(Debug, Clone, PartialEq, Default)]
818pub struct Drawable {
819    /// Every patch's points, concatenated - and every one of them a location.
820    pub points: Vec<Point>,
821    /// Triples into `points`.
822    pub triangles: Vec<[u32; 3]>,
823    /// Pairs into `points`: the segments of every polyline.
824    pub lines: Vec<[u32; 2]>,
825    /// Nodes left out because they hold no location - a `NaN` corner of a grid
826    /// with holes, the padding of a short parametric line.
827    ///
828    /// Counted rather than swallowed: a representation whose nodes are *all*
829    /// undefined comes back empty, and empty is exactly what a reader that
830    /// found nothing also returns. This is what tells the two apart.
831    pub dropped: usize,
832    /// The **cell** each triangle was cut from - `triangle_cells[i]` for
833    /// `triangles[i]` - in the numbering of [`Geometry::cell_count`], which is
834    /// the numbering a per-cell property's values line up against.
835    ///
836    /// This is what lets a viewer paint a flat colour per cell: three.js
837    /// colours vertices, so an aplat needs the geometry de-indexed - each
838    /// triangle its own three vertices - and each new vertex needs to know
839    /// which cell's value it wears. Deriving this table outside the merge
840    /// would mean re-deriving the fan of a polygon, the split of a quad and
841    /// the row order of a lattice, and agreeing with them forever - the exact
842    /// pair of readers this repository refuses.
843    ///
844    /// **Empty when the mapping does not exist**, never guessed: a volume is
845    /// drawn by its boundary skin, and a boundary face is a *face*, not a
846    /// cell - naming one per triangle would line a property's values up
847    /// against elements they were not measured on. Empty and full are the two
848    /// honest states; a caller that finds it empty refuses per-cell painting
849    /// by name, exactly as it did before this field existed.
850    pub triangle_cells: Vec<u32>,
851    /// The cell each **node** belongs to, where that is unique - one entry per
852    /// point, [`Drawable::NO_CELL`] where it is not.
853    ///
854    /// The unique case is a polyline: every node of a run belongs to that run
855    /// and no other, so painting a `PolylineSetRepresentation` per cell - one
856    /// colour a polyline - needs no de-indexing at all: segments of one run
857    /// share vertices and share a colour, and two runs never share a vertex.
858    /// A surface's node is the opposite - shared between cells, which is
859    /// exactly why faces go through [`Drawable::triangle_cells`] and a
860    /// de-index instead. `NO_CELL` there is the honest entry, and a viewer's
861    /// out-of-range guard turns it into the transparent it already gives a
862    /// missing value.
863    pub node_cells: Vec<u32>,
864}
865
866impl Drawable {
867    /// The entry of [`Drawable::node_cells`] for a node that belongs to no
868    /// single cell - a surface node, shared between the cells around it.
869    pub const NO_CELL: u32 = u32::MAX;
870}
871
872/// The geometry of a representation, using the readers this build ships.
873pub fn of(representation: &dyn Reflect, source: &dyn ValueSource) -> Result<Geometry> {
874    of_with(crate::readers::default_readers(), representation, source)
875}
876
877/// The geometry of a representation, using a registry the caller controls.
878///
879/// The entry point for an application that has taught the crate a
880/// representation it does not know - see [`crate::readers`].
881pub fn of_with(
882    readers: &Readers,
883    representation: &dyn Reflect,
884    source: &dyn ValueSource,
885) -> Result<Geometry> {
886    let info = representation.type_info();
887    let Some(reader) = readers.find(info.type_key) else {
888        return Err(Error::unsupported(leak(info.type_key), info.name));
889    };
890    let patches = reader.read(representation, source)?;
891    // The crate's rule, applied to every reader at once: a representation that
892    // evaluates to nothing is an error naming it, never a silent empty result.
893    // The corpus has a real case - a `TriangulatedSetRepresentation` of 891
894    // bytes with no `TrianglePatch` at all - and "no geometry" must not look
895    // like a working read of a representation with nothing to draw.
896    if patches.is_empty() {
897        return Err(Error::malformed(
898            info.name,
899            "the representation declares no patch at all",
900        ));
901    }
902    Ok(Geometry {
903        kind: info.name,
904        patches,
905    })
906}
907
908/// Whether this build knows this representation, without reading anything.
909///
910/// What a listing uses to grey out the rows it cannot show.
911pub fn is_supported(representation: &dyn Reflect) -> bool {
912    supports(representation.type_info().type_key)
913}
914
915/// [`is_supported`] from the concept's name alone - `TriangulatedSetRepresentation`,
916/// without `obj_` - so a listing can ask **before** parsing the object.
917///
918/// The same answer by construction: `is_supported` is this, on the type key the
919/// object would report. What it buys is measured: `drawable` over a package of
920/// 77 090 objects parsed every one of them to keep 5 402 (measured on a large
921/// package).
922pub fn supports(type_key: &str) -> bool {
923    crate::readers::default_readers().handles(type_key)
924}
925
926/// How many nodes the document states this representation holds - no read.
927///
928/// The pair of [`is_supported`]: that one says *can this be drawn at all*, from
929/// the concept's name alone; this one says *how big is it*, from what the
930/// document states about itself. Together they let a viewer list a thousand
931/// representations without touching the data file, which is the difference
932/// between a package that opens and one that appears to hang.
933///
934/// `None` when nothing states it. Read
935/// [`RepresentationReader::declared_nodes`] before showing the number: it is
936/// what the document *claims*, not what [`of`] will produce, and the two differ
937/// wherever the reader drops or derives nodes.
938///
939/// [`RepresentationReader::declared_nodes`]: crate::readers::RepresentationReader::declared_nodes
940pub fn declared_nodes(representation: &dyn Reflect) -> Option<u64> {
941    crate::readers::default_readers().declared_nodes(representation)
942}
943
944/// Every reader this crate implements.
945///
946/// One entry per concept rather than one per version: `type_key` is the same
947/// across versions by construction, so these cover RESQML 2.0.1 and 2.2 alike,
948/// and the next release too unless it respells the insides.
949pub(crate) fn builtin_readers() -> Vec<Box<dyn RepresentationReader>> {
950    /// A reader defined by a concept name and a function.
951    struct Builtin {
952        name: &'static str,
953        key: &'static str,
954        read: fn(&dyn Reflect, &dyn ValueSource) -> Result<Vec<Patch>>,
955        /// What the document states this concept holds, when it states it.
956        ///
957        /// `None` here means the concept says nothing a listing could use, and
958        /// most of them do not - see [`RepresentationReader::declared_nodes`].
959        nodes: Option<fn(&dyn Reflect) -> Option<u64>>,
960    }
961
962    impl RepresentationReader for Builtin {
963        fn name(&self) -> &'static str {
964            self.name
965        }
966        fn handles(&self, type_key: &str) -> bool {
967            type_key == self.key
968        }
969        fn read(&self, representation: &dyn Reflect, source: &dyn ValueSource) -> Result<Vec<Patch>> {
970            (self.read)(representation, source)
971        }
972        fn declared_nodes(&self, representation: &dyn Reflect) -> Option<u64> {
973            (self.nodes?)(representation)
974        }
975    }
976
977    macro_rules! reader {
978        ($key:literal, $f:path) => {
979            reader!($key, $f, None)
980        };
981        ($key:literal, $f:path, $nodes:expr) => {
982            Box::new(Builtin {
983                name: $key,
984                key: $key,
985                read: $f,
986                nodes: $nodes,
987            }) as Box<dyn RepresentationReader>
988        };
989    }
990
991    // The second argument is deliberately absent more often than not: a concept
992    // that keeps its node count in an array - a `PolylineSetRepresentation`'s
993    // `NodeCountPerPolyline` - or that states elements rather than nodes - a
994    // `SubRepresentation`, an IJK grid drawn by its skin - has nothing to say
995    // here that would not have to be measured anyway.
996    vec![
997        reader!(
998            "TriangulatedSetRepresentation",
999            triangulated,
1000            Some(triangulated_nodes)
1001        ),
1002        reader!("PointSetRepresentation", point_set, Some(patch_count_nodes)),
1003        reader!("PolylineRepresentation", polyline, Some(patch_count_nodes)),
1004        reader!("PolylineSetRepresentation", polyline_set),
1005        reader!("Grid2dRepresentation", grid_2d, Some(grid_2d_nodes)),
1006        reader!(
1007            "WellboreFrameRepresentation",
1008            crate::well::frame,
1009            Some(stated_node_count)
1010        ),
1011        reader!("IjkGridRepresentation", crate::ijk::read),
1012        reader!(
1013            "WellboreTrajectoryRepresentation",
1014            trajectory,
1015            Some(trajectory_nodes)
1016        ),
1017        reader!("SubRepresentation", crate::parent::sub_representation),
1018        reader!(
1019            "UnstructuredGridRepresentation",
1020            crate::unstructured::read,
1021            Some(stated_node_count)
1022        ),
1023        reader!("PlaneSetRepresentation", crate::plane::read),
1024        reader!("GridConnectionSetRepresentation", crate::connection::read),
1025        // Not a RESQML representation: a WITSML trajectory, drawn from its
1026        // stations (`crate::stations`). Keyed on the concept like the rest, so
1027        // a WITSML 2.0 `Trajectory` reaches it too - and is refused there by
1028        // name, having no `DefaultMdDatum` to count its depths from.
1029        reader!(
1030            "Trajectory",
1031            crate::stations::read,
1032            Some(crate::stations::declared)
1033        ),
1034    ]
1035}
1036
1037// --- What the document states, with nothing read ---------------------------
1038//
1039// Each of these reads by the **field present**, never by the release: a
1040// `TrianglePatch` spells `NodeCount` the same way in RESQML 2.0.1 and 2.2, and
1041// a concept that drops the field simply stops answering. Spelling a version
1042// here would give one release a number and the other a silence, which reads as
1043// a package holding nothing.
1044
1045/// `NodeCount`, wherever the concept hangs it.
1046///
1047/// A `WellboreFrameRepresentation` states it on the object; an
1048/// `UnstructuredGridRepresentation` states `CellCount` there and puts
1049/// `NodeCount` on its `Geometry`, beside the `Points` it counts. Both are read
1050/// by looking for the field, which is also what makes this survive a release
1051/// that moves it - the mistake `grid_2d` already had to accommodate.
1052fn stated_node_count(representation: &dyn Reflect) -> Option<u64> {
1053    let count = crate::values::scalar_i64(representation, "NodeCount").or_else(|| {
1054        let geometry = field_object(representation, "Geometry")?;
1055        crate::values::scalar_i64(geometry, "NodeCount")
1056    })?;
1057    non_negative(count)
1058}
1059
1060/// A trajectory's `KnotCount`: the control points of its parametric line.
1061///
1062/// The line is a **single** one, which is what makes this answerable - the
1063/// `KnotCount` of a line *set* is the longest line's, and the short ones are
1064/// padded with `NaN`, so a set states an upper bound and not a count (see
1065/// `parametric.rs`). A trajectory with no `Geometry` at all falls back to its
1066/// datum and says nothing here.
1067fn trajectory_nodes(representation: &dyn Reflect) -> Option<u64> {
1068    let geometry = field_object(representation, "Geometry")?;
1069    non_negative(crate::values::scalar_i64(geometry, "KnotCount")?)
1070}
1071
1072/// The nodes of every `TrianglePatch`, which each state their own.
1073fn triangulated_nodes(representation: &dyn Reflect) -> Option<u64> {
1074    sum_over(representation, &["TrianglePatch"], "NodeCount")
1075}
1076
1077/// The `Count` of every node patch: *number of items in the patch*.
1078///
1079/// RESQML 2.2's `NodePatchGeometry` carries no count, so a 2.2 point set
1080/// answers nothing rather than zero.
1081fn patch_count_nodes(representation: &dyn Reflect) -> Option<u64> {
1082    sum_over(representation, NODE_PATCH, "Count")
1083}
1084
1085/// A lattice states both its axes, and their product is its nodes.
1086///
1087/// **Declared, not drawn.** A node of the lattice may carry no location - that
1088/// is ALWYN's every-other-node horizons - and the reader drops those. So this
1089/// is an upper bound on what will appear, which is what a size hint is for.
1090fn grid_2d_nodes(representation: &dyn Reflect) -> Option<u64> {
1091    // 2.0.1 puts the two counts on `Grid2dPatch`, 2.2 on the representation:
1092    // the same fallback `grid_2d` itself uses, so the listing and the reader
1093    // cannot disagree about where they are.
1094    let patch = field_object(representation, "Grid2dPatch").unwrap_or(representation);
1095    let fastest = non_negative(crate::values::scalar_i64(patch, "FastestAxisCount")?)?;
1096    let slowest = non_negative(crate::values::scalar_i64(patch, "SlowestAxisCount")?)?;
1097    fastest.checked_mul(slowest)
1098}
1099
1100/// Add one stated field across a concept's patches, whatever they are called.
1101///
1102/// `None` when no patch states it: a sum over nothing is zero, and zero is a
1103/// claim about the contents rather than about what the document says.
1104fn sum_over(representation: &dyn Reflect, patches: &[&str], field: &str) -> Option<u64> {
1105    let mut total = 0u64;
1106    let mut stated = false;
1107    for name in patches {
1108        for patch in list(representation, name) {
1109            let Some(count) = crate::values::scalar_i64(patch, field) else {
1110                continue;
1111            };
1112            total = total.checked_add(non_negative(count)?)?;
1113            stated = true;
1114        }
1115    }
1116    stated.then_some(total)
1117}
1118
1119/// A stated count that is negative is not a count; it is a broken document.
1120fn non_negative(value: i64) -> Option<u64> {
1121    u64::try_from(value).ok()
1122}
1123
1124// ---------------------------------------------------------------------------
1125
1126/// One patch of a representation, whichever way its version wraps it.
1127///
1128/// RESQML 2.0.1 puts a `PointGeometry` under a patch element - `NodePatch`,
1129/// `LinePatch`, `TrianglePatch` - as a child called `Geometry`. RESQML 2.2
1130/// sometimes drops the wrapper and names the geometry itself, as in
1131/// `NodePatchGeometry`. The two are told apart by looking for `Points`: an
1132/// element that has one *is* the geometry.
1133///
1134/// The distinction cost real points. Reading only the 2.0.1 spelling gave a 2.2
1135/// point set zero patches, and zero patches is a successful call returning
1136/// nothing to draw - so [`patch_points`] refuses a patch with no geometry
1137/// rather than yielding an empty vector.
1138struct PatchParts<'a> {
1139    /// The element carrying the sibling fields - triangle indices, node counts.
1140    owner: &'a dyn Reflect,
1141    /// The `PointGeometry`, if the patch declares one.
1142    geometry: Option<&'a dyn Reflect>,
1143}
1144
1145fn patches_of<'a>(representation: &'a dyn Reflect, fields: &[&str]) -> Vec<PatchParts<'a>> {
1146    let mut out = Vec::new();
1147    for field in fields {
1148        for item in list(representation, field) {
1149            // An element holding `Points` *is* the geometry; one holding a
1150            // `Geometry` child is a wrapper around it.
1151            let geometry = if field_object(item, "Points").is_some() {
1152                Some(item)
1153            } else {
1154                field_object(item, "Geometry")
1155            };
1156            out.push(PatchParts {
1157                owner: item,
1158                geometry,
1159            });
1160        }
1161        if !out.is_empty() {
1162            return out;
1163        }
1164    }
1165    out
1166}
1167
1168fn triangulated(representation: &dyn Reflect, source: &dyn ValueSource) -> Result<Vec<Patch>> {
1169    let mut out = Vec::new();
1170    let mut node_offset = 0usize;
1171    for (index, patch) in patches_of(representation, &["TrianglePatch"])
1172        .into_iter()
1173        .enumerate()
1174    {
1175        let at = format!("TrianglePatch[{index}]");
1176        let points = patch_points(&patch, source, &at)?;
1177        let patch = patch.owner;
1178
1179        let triangles = match field_object(patch, "Triangles") {
1180            Some(array) => {
1181                let flat = values::read_integers(array, source, &format!("{at}.Triangles"))?;
1182                if flat.len() % 3 != 0 {
1183                    return Err(Error::malformed(
1184                        &at,
1185                        format!("{} index/indices is not a whole number of triangles", flat.len()),
1186                    ));
1187                }
1188                let base = node_offset_used(&flat, node_offset, points.len(), &at)?;
1189                flat.as_chunks::<3>()
1190                    .0
1191                    .iter()
1192                    .map(|c| [(c[0] - base) as u32, (c[1] - base) as u32, (c[2] - base) as u32])
1193                    .collect()
1194            }
1195            None => Vec::new(),
1196        };
1197        node_offset += points.len();
1198        out.push(Patch {
1199            index,
1200            node_offset: node_offset - points.len(),
1201            points,
1202            topology: Topology::Triangles(triangles),
1203            closed: Vec::new(),
1204        });
1205    }
1206    Ok(out)
1207}
1208
1209/// Which numbering a patch's triangle indices are written in.
1210///
1211/// RESQML calls a triangulated set *"a representation … which gets its geometry
1212/// from a 1D array of points"*, and the two reference implementations read that
1213/// sentence differently: FESAPI writes indices into that single array - patch 1
1214/// of a 5-node patch 0 starts at 5 - while resqpy writes them relative to each
1215/// patch and adds the offset only when it merges. Both kinds of file exist, so
1216/// this decides from the numbers instead of picking a side, and returns the base
1217/// to subtract.
1218///
1219/// The two agree on patch 0, where the offset is zero. Where they differ, only
1220/// one of the two ranges can contain every index; an array that fits neither is
1221/// a broken file and is refused rather than drawn.
1222fn node_offset_used(indices: &[i64], offset: usize, points: usize, at: &str) -> Result<i64> {
1223    let Some(&min) = indices.iter().min() else {
1224        return Ok(0);
1225    };
1226    let Some(&max) = indices.iter().max() else {
1227        return Ok(0);
1228    };
1229    let (offset, points) = (offset as i64, points as i64);
1230
1231    if min >= 0 && max < points {
1232        return Ok(0);
1233    }
1234    if min >= offset && max < offset + points {
1235        return Ok(offset);
1236    }
1237    Err(Error::malformed(
1238        at,
1239        format!(
1240            "triangle indices run {min}..={max}, which is neither this patch's own \
1241             0..{points} nor its place {offset}..{} in the representation's nodes",
1242            offset + points
1243        ),
1244    ))
1245}
1246
1247/// The field names a version may use for a node patch, most specific first.
1248const NODE_PATCH: &[&str] = &["NodePatch", "NodePatchGeometry"];
1249
1250fn point_set(representation: &dyn Reflect, source: &dyn ValueSource) -> Result<Vec<Patch>> {
1251    let mut out = Vec::new();
1252    let mut node_offset = 0usize;
1253    for (index, patch) in patches_of(representation, NODE_PATCH).into_iter().enumerate() {
1254        let at = format!("NodePatch[{index}]");
1255        let points = patch_points(&patch, source, &at)?;
1256        let start = node_offset;
1257        node_offset += points.len();
1258        out.push(Patch {
1259            index,
1260            node_offset: start,
1261            points,
1262            topology: Topology::Points,
1263            closed: Vec::new(),
1264        });
1265    }
1266    Ok(out)
1267}
1268
1269fn polyline(representation: &dyn Reflect, source: &dyn ValueSource) -> Result<Vec<Patch>> {
1270    let mut out = Vec::new();
1271    let mut node_offset = 0usize;
1272    for (index, patch) in patches_of(representation, NODE_PATCH).into_iter().enumerate() {
1273        let at = format!("NodePatch[{index}]");
1274        let points = patch_points(&patch, source, &at)?;
1275        // One patch is one run of consecutive nodes. `IsClosed` says whether
1276        // the last joins the first, and it is left to the caller - see
1277        // `Topology::Polylines`.
1278        let length = points.len() as u32;
1279        let start = node_offset;
1280        node_offset += points.len();
1281        out.push(Patch {
1282            index,
1283            node_offset: start,
1284            points,
1285            topology: Topology::Polylines(vec![length]),
1286            closed: Vec::new(),
1287        });
1288    }
1289    Ok(out)
1290}
1291
1292fn polyline_set(representation: &dyn Reflect, source: &dyn ValueSource) -> Result<Vec<Patch>> {
1293    let mut out = Vec::new();
1294    let mut node_offset = 0usize;
1295    for (index, patch) in patches_of(representation, &["LinePatch"]).into_iter().enumerate() {
1296        let at = format!("LinePatch[{index}]");
1297        let points = patch_points(&patch, source, &at)?;
1298        // `NodeCountPerPolyline` says where one line ends and the next begins;
1299        // without it the patch is one run.
1300        let runs = match field_object(patch.owner, "NodeCountPerPolyline") {
1301            Some(array) => values::read_integers(array, source, &at)?
1302                .into_iter()
1303                .map(|n| n.max(0) as u32)
1304                .collect(),
1305            None => vec![points.len() as u32],
1306        };
1307        // **And which ones close.** A separate array that this reader used
1308        // to ignore: a closed polyline came out without its last segment and
1309        // was drawn open (seen on Volve). Empty when the file says nothing,
1310        // which is not *all open*: a producer that omits `ClosedPolylines`
1311        // has said nothing, and closing anyway would invent a ring.
1312        let closed = match field_object(patch.owner, "ClosedPolylines") {
1313            Some(array) => {
1314                let mut flags = values::read_flags(array, source, &format!("{at}.ClosedPolylines"))?;
1315                // An array that states more flags than there are lines is
1316                // truncated; one that states fewer leaves the last lines
1317                // unanswered rather than inventing an answer for them.
1318                flags.truncate(runs.len());
1319                flags
1320            }
1321            None => Vec::new(),
1322        };
1323        let start = node_offset;
1324        node_offset += points.len();
1325        out.push(Patch {
1326            index,
1327            node_offset: start,
1328            points,
1329            topology: Topology::Polylines(runs),
1330            closed,
1331        });
1332    }
1333    Ok(out)
1334}
1335
1336/// A wellbore trajectory: one parametric line, at its own knots.
1337///
1338/// The knots are the stations the well was surveyed at, and they are what comes
1339/// out - not a resampling of the curve between them. Two reasons, and the
1340/// second is the binding one: a spline passes through its own knots, so
1341/// sampling between them changes how smooth the drawn path looks and nothing
1342/// about where it goes; and the nodes of a trajectory are what its properties
1343/// are indexed by, so adding points would move every log value off its station.
1344///
1345/// The line is still *evaluated* rather than its control points copied out. It
1346/// matters for a vertical well, where the schema fills the third coordinate
1347/// from the parameter and leaves the control point's own slot meaningless -
1348/// see [`crate::parametric::Line::stations`].
1349fn trajectory(representation: &dyn Reflect, source: &dyn ValueSource) -> Result<Vec<Patch>> {
1350    let at = "WellboreTrajectoryRepresentation";
1351    let Some(geometry) = field_object(representation, "Geometry") else {
1352        return crate::well::datum_trajectory(representation, source, at);
1353    };
1354    let points = crate::parametric::Line::single(geometry, source, at)?.stations();
1355    if points.is_empty() {
1356        return Err(Error::malformed(at, "a trajectory with no control point"));
1357    }
1358    let length = points.len() as u32;
1359    Ok(vec![Patch {
1360        index: 0,
1361        node_offset: 0,
1362        points,
1363        topology: Topology::Polylines(vec![length]),
1364        closed: Vec::new(),
1365    }])
1366}
1367
1368fn grid_2d(representation: &dyn Reflect, source: &dyn ValueSource) -> Result<Vec<Patch>> {
1369    let patch = field_object(representation, "Grid2dPatch").unwrap_or(representation);
1370    let points = geometry_points(patch, source, "Grid2dPatch")?;
1371    let fastest = values::scalar_i64(patch, "FastestAxisCount").unwrap_or(0).max(0) as usize;
1372    let slowest = values::scalar_i64(patch, "SlowestAxisCount").unwrap_or(0).max(0) as usize;
1373
1374    // The two counts and the point count must agree, and when they do not it is
1375    // the file that is wrong - worth saying rather than drawing a wrong grid.
1376    let mut points = points;
1377    if fastest * slowest != points.len() {
1378        // One known writer disagrees in a provable way: SKUA-GOCAD sizes its
1379        // lattice `Spacing.Count` in points instead of intervals, so the
1380        // evaluated lattice is exactly one point too large per axis. When the
1381        // arithmetic pins it - (fastest+1) × (slowest+1) points - the extra
1382        // row and column are dropped; see `points::drop_last_per_axis`.
1383        if (fastest + 1) * (slowest + 1) == points.len() {
1384            points = crate::points::drop_last_per_axis(points, &[slowest + 1, fastest + 1]);
1385        } else {
1386            return Err(Error::malformed(
1387                "Grid2dPatch",
1388                format!(
1389                    "{fastest} × {slowest} axes describe {} point(s), the geometry has {}",
1390                    fastest * slowest,
1391                    points.len()
1392                ),
1393            ));
1394        }
1395    }
1396    Ok(vec![Patch {
1397        index: 0,
1398        node_offset: 0,
1399        points,
1400        topology: Topology::Grid { fastest, slowest },
1401        closed: Vec::new(),
1402    }])
1403}
1404
1405// ---------------------------------------------------------------------------
1406
1407/// The points of one patch.
1408///
1409/// Every step of the way out is an error, never an empty vector: a patch with
1410/// no geometry, or a geometry with no `Points`, means this code did not find
1411/// what the file holds. Returning nothing would be reported as a successful
1412/// read of a representation with nothing to draw.
1413fn patch_points(patch: &PatchParts<'_>, source: &dyn ValueSource, at: &str) -> Result<Vec<Point>> {
1414    let Some(geometry) = patch.geometry else {
1415        return Err(Error::malformed(at, "a patch that declares no geometry"));
1416    };
1417    let Some(array) = field_object(geometry, "Points") else {
1418        return Err(Error::malformed(at, "a geometry with no `Points`"));
1419    };
1420    points::read(
1421        array,
1422        source,
1423        &format!("{at}.Geometry.Points"),
1424        crate::parametric::Vertical::read(geometry, source),
1425    )
1426}
1427
1428/// The points of a `PointGeometry` hanging directly on an element.
1429fn geometry_points(owner: &dyn Reflect, source: &dyn ValueSource, at: &str) -> Result<Vec<Point>> {
1430    let geometry = match field_object(owner, "Geometry") {
1431        Some(geometry) => geometry,
1432        // 2.2 hangs the geometry on the representation itself.
1433        None if field_object(owner, "Points").is_some() => owner,
1434        None => return Err(Error::malformed(at, "a patch that declares no geometry")),
1435    };
1436    let Some(array) = field_object(geometry, "Points") else {
1437        return Err(Error::malformed(at, "a geometry with no `Points`"));
1438    };
1439    points::read(
1440        array,
1441        source,
1442        &format!("{at}.Geometry.Points"),
1443        crate::parametric::Vertical::read(geometry, source),
1444    )
1445}
1446
1447/// A field's objects, whether the schema made it a list or a single element.
1448///
1449/// Shared with the readers that live in their own modules - one implementation
1450/// of *what does this field hold*, because two would differ the first time one
1451/// of them met a field the schema declares singular and a file repeats.
1452pub(crate) fn field_list<'a>(object: &'a dyn Reflect, field: &str) -> Vec<&'a dyn Reflect> {
1453    list(object, field)
1454}
1455
1456fn list<'a>(object: &'a dyn Reflect, field: &str) -> Vec<&'a dyn Reflect> {
1457    match object.field(field) {
1458        Some(FieldValue::List(items)) => items.iter().filter_map(|item| item.as_object()).collect(),
1459        Some(FieldValue::Object(one)) => vec![one],
1460        _ => Vec::new(),
1461    }
1462}
1463
1464pub(crate) fn field_object<'a>(object: &'a dyn Reflect, field: &str) -> Option<&'a dyn Reflect> {
1465    match object.field(field)? {
1466        FieldValue::Object(child) => Some(child),
1467        _ => None,
1468    }
1469}
1470
1471/// `type_key` is `&'static str` already; this only re-borrows it for the error.
1472pub(crate) fn leak(key: &str) -> &'static str {
1473    // Every `type_key` comes from a `&'static TypeInfo`, so the lifetime is
1474    // real; the signature of `field` just does not carry it.
1475    Box::leak(key.to_string().into_boxed_str())
1476}
1477
1478#[cfg(test)]
1479mod tests {
1480    use super::*;
1481
1482    #[test]
1483    fn the_first_patch_reads_the_same_either_way() {
1484        // Offset zero: the two conventions coincide, and nothing has to be
1485        // guessed.
1486        assert_eq!(node_offset_used(&[0, 1, 3, 1, 4, 3], 0, 5, "t").unwrap(), 0);
1487    }
1488
1489    #[test]
1490    fn a_later_patch_written_by_fesapi_is_offset() {
1491        // testingPackageCpp.epc, patch 1 of ea6943d0: five nodes numbered 5..9.
1492        assert_eq!(
1493            node_offset_used(&[5, 6, 8, 6, 9, 8, 6, 7, 9], 5, 5, "t").unwrap(),
1494            5
1495        );
1496    }
1497
1498    #[test]
1499    fn a_later_patch_written_by_resqpy_is_not() {
1500        // The same patch with resqpy's numbering: indices fit its own points,
1501        // so no offset is removed.
1502        assert_eq!(node_offset_used(&[0, 1, 3, 1, 4, 3], 5, 5, "t").unwrap(), 0);
1503    }
1504
1505    #[test]
1506    fn an_index_belonging_to_neither_is_refused() {
1507        // Drawing this would use whatever point happened to sit at that slot.
1508        let refused = node_offset_used(&[0, 1, 99], 5, 5, "TrianglePatch[1]");
1509        assert!(refused.is_err(), "{refused:?}");
1510    }
1511
1512    #[test]
1513    fn merging_renumbers_into_one_buffer() {
1514        let geometry = Geometry {
1515            kind: "TriangulatedSetRepresentation",
1516            patches: vec![
1517                Patch {
1518                    index: 0,
1519                    node_offset: 0,
1520                    points: vec![[0.0; 3], [1.0; 3], [2.0; 3]],
1521                    topology: Topology::Triangles(vec![[0, 1, 2]]),
1522                    closed: Vec::new(),
1523                },
1524                Patch {
1525                    index: 1,
1526                    node_offset: 3,
1527                    points: vec![[3.0; 3], [4.0; 3], [5.0; 3]],
1528                    topology: Topology::Triangles(vec![[0, 1, 2]]),
1529                    closed: Vec::new(),
1530                },
1531            ],
1532        };
1533        let (points, triangles) = geometry.merged();
1534        assert_eq!(points.len(), 6);
1535        assert_eq!(triangles, vec![[0, 1, 2], [3, 4, 5]]);
1536    }
1537}