Skip to main content

energyml_geometry/
defined.rs

1//! Nodes that have no position, and what a drawing may do with them.
2//!
3//! # What the standard says, and what it does not
4//!
5//! Energistics fixes the null value of a floating-point array without
6//! ambiguity: *"An array of double values provided explicitly by an HDF5
7//! dataset. **By convention, the null value is NaN.**"* (Energistics common
8//! TRG v2.3 §6.31 `FloatingPointExternalArray`, and the same sentence on
9//! RESQML 2.0.1's `DoubleHdf5Array`). Contrast the integer case, where the null
10//! *"must be explicitly provided in the `NullValue` attribute"* (§6.41) - for
11//! doubles it is NaN, always, and no attribute says otherwise.
12//!
13//! So a `Point3dZValueArray` whose `ZValues` holds NaN is holding **nulls**,
14//! and that much is quotable. What is *not* quotable is the next step: no
15//! clause in RESQML 2.0.1 or 2.2 says that a null Z makes a node undefined, and
16//! none says that a cell touching an undefined node must not be drawn. For
17//! `Grid2dRepresentation` the specification is entirely silent; the mechanism
18//! it does give for *"the surface stops here"* is topological -
19//! `PatchBoundaries`' `OuterRing` / `InnerRing`, *"inside the ring, the patch
20//! is defined, outside it is not"* (RESQML TRG v2.2 §11.12).
21//!
22//! For column-layer grids the standard **is** explicit, and it is worth quoting
23//! because it settles what wins:
24//!
25//! > `PillarGeometryIsDefined` - *"Indicator that a pillar has at least one
26//! > node with a defined cell geometry. **If the indicator does not indicate
27//! > that the pillar geometry is defined, then this over-rides any other node
28//! > geometry specification.**"* (TRG v2.2 §5.1, mandatory)
29//!
30//! > `CellGeometryIsDefined` - *"Indicator that a cell has a defined geometry.
31//! > **If the indicator shows that the cell geometry is NOT defined, then this
32//! > attribute overrides any other node geometry specification.**"* (§5.1,
33//! > optional)
34//!
35//! Note what those do not say: they never promise the coordinates will be NaN.
36//! They say the **flag beats the coordinates**. See [`crate::ijk`], which reads
37//! them.
38//!
39//! # What this module decides, and on what ground
40//!
41//! A node whose position is NaN has **no position**. That is not an
42//! interpretation of RESQML, it is arithmetic: there is no point in space to
43//! put it at, no edge to draw to it, and no cell it can be a corner of. Every
44//! output format this workspace writes demands three numbers per vertex, and
45//! two of the three have no syntax for a missing one - GeoJSON is JSON, and
46//! **`NaN` is not a JSON value**, so a single null Z made the whole file
47//! unparseable by `JSON.parse`.
48//!
49//! Therefore: **an undefined node is dropped, and so is everything indexed on
50//! it.** Recorded here as a reader's decision rather than dressed up as a
51//! citation.
52//!
53//! Two consequences that are deliberate, not oversights:
54//!
55//! - **A lattice with a hole is not a lattice.** [`Topology::Grid`] promises a
56//!   `fastest × slowest` rectangle, row by row; drop one node and every index
57//!   after it means something else. So a grid that loses a node becomes
58//!   [`Topology::Points`] - a demotion the caller can see, rather than a
59//!   rectangle that lies about its own shape.
60//! - **Properties move with their elements.** Dropping points without dropping
61//!   the matching property values would colour the survivors with their
62//!   neighbours' numbers, which is exactly the plausible-picture-of-nothing this
63//!   workspace refuses elsewhere. [`Compaction`] therefore carries the index
64//!   maps, and [`crate::scene::SceneObject::drawable_only`] uses them.
65
66use crate::geometry::{Geometry, Patch, Topology};
67use crate::points::Point;
68
69/// Whether a point can be drawn: three finite numbers.
70///
71/// Infinity is refused alongside NaN. It is not a null the standard defines,
72/// but it is equally undrawable, and a viewer handed one will either clip the
73/// scene to nothing or refuse the file.
74pub fn is_defined(point: &Point) -> bool {
75    point.iter().all(|c| c.is_finite())
76}
77
78/// A geometry with its undefined nodes removed, and the maps to follow it.
79///
80/// The maps are in **merged order** - the order [`Geometry::merged`] and
81/// [`Geometry::cell_count`] produce, patch after patch - because that is the
82/// order an attribute's values are in.
83#[derive(Debug, Clone, PartialEq)]
84pub struct Compaction {
85    /// The geometry, with undefined nodes and everything touching them gone.
86    pub geometry: Geometry,
87    /// For each surviving point, its index in the original merged order.
88    pub points: Vec<usize>,
89    /// For each surviving cell, its index in the original cell order.
90    pub cells: Vec<usize>,
91    /// How many nodes had no position.
92    pub dropped_points: usize,
93    /// How many cells touched one.
94    pub dropped_cells: usize,
95}
96
97impl Geometry {
98    /// How many of this geometry's nodes have no position.
99    pub fn undefined_count(&self) -> usize {
100        self.patches
101            .iter()
102            .flat_map(|patch| patch.points.iter())
103            .filter(|point| !is_defined(point))
104            .count()
105    }
106
107    /// This geometry with every undefined node removed - or `None` when there
108    /// is none, which is the ordinary case and costs one pass and no copy.
109    ///
110    /// `None` rather than a clone of `self` on purpose: the caller can then
111    /// keep borrowing the original, and the overwhelmingly common package pays
112    /// nothing for a rule that exists for the others.
113    pub fn defined_only(&self) -> Option<Compaction> {
114        if self.undefined_count() == 0 {
115            return None;
116        }
117
118        let mut patches = Vec::with_capacity(self.patches.len());
119        let mut kept_points = Vec::new();
120        let mut kept_cells = Vec::new();
121        let mut point_base = 0usize;
122        let mut cell_base = 0usize;
123        let mut node_offset = 0usize;
124        let mut dropped_points = 0usize;
125        let mut dropped_cells = 0usize;
126
127        for patch in &self.patches {
128            // An unstructured grid is left **whole**, points included. Its
129            // topology is a face/node index structure with shared faces, and
130            // renumbering it correctly means rebuilding the face table and the
131            // cell-to-face map. Dropping its points without that would leave
132            // every face pointing at the wrong node - far worse than leaving
133            // one undrawable cell in place. Refused loudly rather than
134            // half-done; see the module documentation.
135            let compactable = !matches!(patch.topology, Topology::Polyhedra(_));
136
137            // `keep[i]` is the new index of old point `i`, or `None`.
138            let mut keep = Vec::with_capacity(patch.points.len());
139            let mut points = Vec::with_capacity(patch.points.len());
140            for (index, point) in patch.points.iter().enumerate() {
141                if !compactable || is_defined(point) {
142                    keep.push(Some(points.len() as u32));
143                    points.push(*point);
144                    kept_points.push(point_base + index);
145                } else {
146                    keep.push(None);
147                    dropped_points += 1;
148                }
149            }
150
151            let (topology, kept_here, dropped_here) = compact_topology(&patch.topology, &keep, cell_base);
152            kept_cells.extend(kept_here);
153            dropped_cells += dropped_here;
154
155            let points_len = points.len();
156            patches.push(Patch {
157                index: patch.index,
158                // Renumbered, because the patches before this one shrank too.
159                // Leaving the original here would send anything that indexes
160                // *nodes of the representation* to the wrong patch entirely.
161                node_offset,
162                points,
163                topology,
164                closed: Vec::new(),
165            });
166            point_base += patch.points.len();
167            cell_base += cells_in(&patch.topology);
168            node_offset += points_len;
169        }
170
171        Some(Compaction {
172            geometry: Geometry {
173                kind: self.kind,
174                patches,
175            },
176            points: kept_points,
177            cells: kept_cells,
178            dropped_points,
179            dropped_cells,
180        })
181    }
182}
183
184/// How many cells a topology has - [`Topology::cell_count`], which became the
185/// one definition when the drawable's cell table needed it too; this wrapper
186/// stays for the callers' sake.
187fn cells_in(topology: &Topology) -> usize {
188    topology.cell_count()
189}
190
191/// Renumber one patch's topology onto the surviving points.
192///
193/// Returns the new topology, the original indices of the cells that survived,
194/// and how many were dropped.
195fn compact_topology(
196    topology: &Topology,
197    keep: &[Option<u32>],
198    cell_base: usize,
199) -> (Topology, Vec<usize>, usize) {
200    let at = |index: u32| keep.get(index as usize).copied().flatten();
201
202    match topology {
203        Topology::Points => (Topology::Points, Vec::new(), 0),
204
205        // A quadrilateral touching an undefined node goes with it, exactly
206        // like a triangle. This is where "the absent cell is not
207        // represented" finally means something visible: a grid drawn as cells
208        // loses the four around a null node, not one dot among 300 000.
209        Topology::Quads(quads) => {
210            let mut out = Vec::with_capacity(quads.len());
211            let mut kept = Vec::with_capacity(quads.len());
212            let mut dropped = 0;
213            for (index, q) in quads.iter().enumerate() {
214                let mut ring = [0u32; 4];
215                let mut whole = true;
216                for (slot, corner) in ring.iter_mut().zip(q.iter()) {
217                    match at(*corner) {
218                        Some(new) => *slot = new,
219                        None => {
220                            whole = false;
221                            break;
222                        }
223                    }
224                }
225                if whole {
226                    out.push(ring);
227                    kept.push(cell_base + index);
228                } else {
229                    dropped += 1;
230                }
231            }
232            (Topology::Quads(out), kept, dropped)
233        }
234
235        // Same rule as a quadrilateral, one length up.
236        Topology::Polygons(rings) => {
237            let mut out = Vec::with_capacity(rings.len());
238            let mut kept = Vec::with_capacity(rings.len());
239            let mut dropped = 0;
240            for (index, ring) in rings.iter().enumerate() {
241                let renumbered: Option<Vec<u32>> = ring.iter().map(|corner| at(*corner)).collect();
242                match renumbered {
243                    Some(ring) => {
244                        out.push(ring);
245                        kept.push(cell_base + index);
246                    }
247                    None => dropped += 1,
248                }
249            }
250            (Topology::Polygons(out), kept, dropped)
251        }
252
253        Topology::Triangles(triangles) => {
254            let mut out = Vec::with_capacity(triangles.len());
255            let mut kept = Vec::with_capacity(triangles.len());
256            let mut dropped = 0;
257            for (index, t) in triangles.iter().enumerate() {
258                match (at(t[0]), at(t[1]), at(t[2])) {
259                    (Some(a), Some(b), Some(c)) => {
260                        out.push([a, b, c]);
261                        kept.push(cell_base + index);
262                    }
263                    _ => dropped += 1,
264                }
265            }
266            (Topology::Triangles(out), kept, dropped)
267        }
268
269        // The same rule as a triangle, one dimension down: an edge touching an
270        // undefined node goes with it, and its cell entry goes too.
271        Topology::Segments(pairs) => {
272            let mut out = Vec::with_capacity(pairs.len());
273            let mut kept = Vec::with_capacity(pairs.len());
274            let mut dropped = 0;
275            for (index, pair) in pairs.iter().enumerate() {
276                match (at(pair[0]), at(pair[1])) {
277                    (Some(a), Some(b)) => {
278                        out.push([a, b]);
279                        kept.push(cell_base + index);
280                    }
281                    _ => dropped += 1,
282                }
283            }
284            (Topology::Segments(out), kept, dropped)
285        }
286
287        // A run through an undefined node is **cut**, not bridged. Joining the
288        // two survivors would draw a segment across the hole - a line the file
289        // never described, and one that looks entirely plausible on screen.
290        //
291        // A cut run becomes several, so the one-value-per-run correspondence a
292        // per-cell property relies on is gone. Rather than guess which piece
293        // inherits the value, every run of an affected patch is reported as
294        // dropped: an attribute on it will be refused by name, which is the
295        // behaviour this workspace prefers to a coloured picture of nothing.
296        Topology::Polylines(runs) => {
297            let mut out = Vec::new();
298            let mut start = 0u32;
299            let mut cut = false;
300            for run in runs {
301                let mut length = 0u32;
302                for offset in 0..*run {
303                    match at(start + offset) {
304                        Some(_) => length += 1,
305                        None => {
306                            if length > 0 {
307                                out.push(length);
308                            }
309                            length = 0;
310                            cut = true;
311                        }
312                    }
313                }
314                if length > 0 {
315                    out.push(length);
316                }
317                start += run;
318            }
319            let dropped = if cut { runs.len() } else { 0 };
320            let kept = if cut {
321                Vec::new()
322            } else {
323                (0..runs.len()).map(|i| cell_base + i).collect()
324            };
325            (Topology::Polylines(out), kept, dropped)
326        }
327
328        Topology::Hexahedra {
329            cells,
330            dimensions,
331            index,
332        } => {
333            let mut out = Vec::with_capacity(cells.len());
334            let mut kept = Vec::with_capacity(cells.len());
335            let mut dropped = 0;
336            for (index, cell) in cells.iter().enumerate() {
337                let mut corners = [0u32; 8];
338                let mut whole = true;
339                for (slot, corner) in corners.iter_mut().zip(cell.iter()) {
340                    match at(*corner) {
341                        Some(new) => *slot = new,
342                        None => {
343                            whole = false;
344                            break;
345                        }
346                    }
347                }
348                if whole {
349                    out.push(corners);
350                    kept.push(cell_base + index);
351                } else {
352                    dropped += 1;
353                }
354            }
355            // `dimensions` is kept as it was: it says what shape the cells were
356            // *built from*, and that stays true. It is no longer the shape of
357            // the cells that remain, and no consumer should read it as such -
358            // the count is `cells.len()`.
359            (
360                Topology::Hexahedra {
361                    cells: out,
362                    dimensions: *dimensions,
363                    // **Filtered with its cell**, never re-derived: this pass
364                    // drops cells too, and a numbering rebuilt from the survivors
365                    // would renumber them onto themselves - which is exactly the
366                    // mistake the numbering exists to prevent. `kept` holds the
367                    // positions that survived, in order.
368                    index: kept
369                        .iter()
370                        .map(|at| index.get(*at).copied().unwrap_or(*at as u32))
371                        .collect(),
372                },
373                kept,
374                dropped,
375            )
376        }
377
378        // A rectangle with a hole in it is not a rectangle. Demoted to loose
379        // points, which is what the survivors honestly are; every cell of the
380        // lattice is reported dropped because none of them can be addressed by
381        // `(i, j)` any more.
382        Topology::Grid { fastest, slowest } => (
383            Topology::Points,
384            Vec::new(),
385            fastest.saturating_sub(1) * slowest.saturating_sub(1),
386        ),
387
388        // Left whole - see the `compactable` guard above, which also keeps this
389        // patch's points, so these indices still address what they always did.
390        Topology::Polyhedra(cells) => (
391            Topology::Polyhedra(cells.clone()),
392            (0..cells.cell_count()).map(|i| cell_base + i).collect(),
393            0,
394        ),
395    }
396}