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}