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}