Skip to main content

plot3d/
face_record.rs

1//! Core data types for face connectivity: [`FaceRecord`], [`FaceMatch`],
2//! [`MatchPoint`], and [`Orientation`].
3//!
4//! # Diagonal Convention
5//!
6//! [`FaceRecord`] stores diagonal corners as `il/jl/kl` (first corner) and
7//! `ih/jh/kh` (second corner). The ordering is **not** normalized: `il` can
8//! be greater than `ih`, encoding that the I-axis is reversed on this face
9//! relative to the matching face on the other block.
10//!
11//! This matches the GridPro/GlennHT connectivity convention and makes it
12//! possible to reconstruct the orientation relationship between two matched
13//! faces from the `FaceRecord` alone, without re-sampling block coordinates.
14//!
15//! When you need min/max values (e.g. for range iteration), use the
16//! normalized accessors: [`FaceRecord::i_lo()`], [`FaceRecord::i_hi()`], etc.
17
18use serde::{Deserialize, Serialize};
19
20use crate::{block::Block, block_face_functions::Face};
21
22/// Compact identifier for a face: `(block_index, il, jl, kl, ih, jh, kh)`.
23pub type FaceKey = (usize, usize, usize, usize, usize, usize, usize);
24
25/// Pointwise correspondence between two block faces.
26#[derive(Clone, Debug, Serialize)]
27pub struct MatchPoint {
28    pub i1: usize,
29    pub j1: usize,
30    pub k1: usize,
31    pub i2: usize,
32    pub j2: usize,
33    pub k2: usize,
34}
35
36/// Extract `(i_lo, i_hi, j_lo, j_hi, k_lo, k_hi)` bounds from a slice of [`MatchPoint`]s.
37///
38/// When `use_block1` is true the block-1 indices (`i1/j1/k1`) are used;
39/// otherwise the block-2 indices (`i2/j2/k2`).
40pub fn match_point_bounds(
41    points: &[MatchPoint],
42    use_block1: bool,
43) -> (usize, usize, usize, usize, usize, usize) {
44    if use_block1 {
45        (
46            points.iter().map(|p| p.i1).min().unwrap(),
47            points.iter().map(|p| p.i1).max().unwrap(),
48            points.iter().map(|p| p.j1).min().unwrap(),
49            points.iter().map(|p| p.j1).max().unwrap(),
50            points.iter().map(|p| p.k1).min().unwrap(),
51            points.iter().map(|p| p.k1).max().unwrap(),
52        )
53    } else {
54        (
55            points.iter().map(|p| p.i2).min().unwrap(),
56            points.iter().map(|p| p.i2).max().unwrap(),
57            points.iter().map(|p| p.j2).min().unwrap(),
58            points.iter().map(|p| p.j2).max().unwrap(),
59            points.iter().map(|p| p.k2).min().unwrap(),
60            points.iter().map(|p| p.k2).max().unwrap(),
61        )
62    }
63}
64
65/// Compact record describing a face on a particular block.
66///
67/// # Diagonal Convention
68///
69/// The fields `il/jl/kl` and `ih/jh/kh` define the two diagonal corners
70/// of this face on the block. These are **NOT** guaranteed to satisfy
71/// `il <= ih`. The ordering encodes **orientation**: when `il > ih`, the
72/// I-axis is reversed on this face relative to the matching face.
73///
74/// Use `i_lo()`/`i_hi()` when you need normalized min/max values
75/// (e.g., for range iteration or face reconstruction).
76#[derive(Clone, Debug, Serialize)]
77pub struct FaceRecord {
78    pub block_index: usize,
79    /// I-index of the first diagonal corner.
80    pub il: usize,
81    /// J-index of the first diagonal corner.
82    pub jl: usize,
83    /// K-index of the first diagonal corner.
84    pub kl: usize,
85    /// I-index of the second diagonal corner.
86    pub ih: usize,
87    /// J-index of the second diagonal corner.
88    pub jh: usize,
89    /// K-index of the second diagonal corner.
90    pub kh: usize,
91    pub id: Option<usize>,
92    /// Which physical axis ('x','y','z') the u-parameter primarily aligns with,
93    /// and whether the physical coordinate increases as the u-index increases.
94    /// `None` when not yet computed.
95    #[serde(default)]
96    pub u_physical: Option<(char, bool)>,
97    /// Same for the v-parameter (second varying index of the face).
98    #[serde(default)]
99    pub v_physical: Option<(char, bool)>,
100}
101
102impl FaceRecord {
103    /// Build a corner description from matching points.
104    ///
105    /// * `block_index` – Owning block index.
106    /// * `points` – Matched nodes.
107    /// * `first` – If `true` we use the indices from block1; otherwise block2.
108    ///
109    /// Returns `None` when `points` is empty.
110    pub(crate) fn from_match_points(
111        block_index: usize,
112        points: &[MatchPoint],
113        first: bool,
114    ) -> Option<Self> {
115        if points.is_empty() {
116            return None;
117        }
118        let il = points
119            .iter()
120            .map(|p| if first { p.i1 } else { p.i2 })
121            .min()?;
122        let jl = points
123            .iter()
124            .map(|p| if first { p.j1 } else { p.j2 })
125            .min()?;
126        let kl = points
127            .iter()
128            .map(|p| if first { p.k1 } else { p.k2 })
129            .min()?;
130        let ih = points
131            .iter()
132            .map(|p| if first { p.i1 } else { p.i2 })
133            .max()?;
134        let jh = points
135            .iter()
136            .map(|p| if first { p.j1 } else { p.j2 })
137            .max()?;
138        let kh = points
139            .iter()
140            .map(|p| if first { p.k1 } else { p.k2 })
141            .max()?;
142        Some(Self {
143            block_index,
144            il,
145            jl,
146            kl,
147            ih,
148            jh,
149            kh,
150            id: None,
151            u_physical: None,
152            v_physical: None,
153        })
154    }
155
156    /// Construct a record from a Face instance.
157    pub fn from_face(face: &Face) -> Self {
158        Self {
159            block_index: face.block_index().unwrap_or(usize::MAX),
160            il: face.imin(),
161            jl: face.jmin(),
162            kl: face.kmin(),
163            ih: face.imax(),
164            jh: face.jmax(),
165            kh: face.kmax(),
166            id: face.id(),
167            u_physical: None,
168            v_physical: None,
169        }
170    }
171
172    // -- Normalized accessors (for range iteration / face reconstruction) --
173
174    /// Smallest I-index. Always `min(il, ih)`.
175    #[inline]
176    pub fn i_lo(&self) -> usize {
177        self.il.min(self.ih)
178    }
179    /// Largest I-index. Always `max(il, ih)`.
180    #[inline]
181    pub fn i_hi(&self) -> usize {
182        self.il.max(self.ih)
183    }
184    /// Smallest J-index.
185    #[inline]
186    pub fn j_lo(&self) -> usize {
187        self.jl.min(self.jh)
188    }
189    /// Largest J-index.
190    #[inline]
191    pub fn j_hi(&self) -> usize {
192        self.jl.max(self.jh)
193    }
194    /// Smallest K-index.
195    #[inline]
196    pub fn k_lo(&self) -> usize {
197        self.kl.min(self.kh)
198    }
199    /// Largest K-index.
200    #[inline]
201    pub fn k_hi(&self) -> usize {
202        self.kl.max(self.kh)
203    }
204
205    /// True when the I-axis is reversed (`il > ih`).
206    #[inline]
207    pub fn i_reversed(&self) -> bool {
208        self.il > self.ih
209    }
210    /// True when the J-axis is reversed (`jl > jh`).
211    #[inline]
212    pub fn j_reversed(&self) -> bool {
213        self.jl > self.jh
214    }
215    /// True when the K-axis is reversed (`kl > kh`).
216    #[inline]
217    pub fn k_reversed(&self) -> bool {
218        self.kl > self.kh
219    }
220
221    /// Ascending bounds: `([lo_i, lo_j, lo_k], [hi_i, hi_j, hi_k])`.
222    #[inline]
223    pub fn bounds(&self) -> ([usize; 3], [usize; 3]) {
224        (
225            [self.i_lo(), self.j_lo(), self.k_lo()],
226            [self.i_hi(), self.j_hi(), self.k_hi()],
227        )
228    }
229
230    /// Index (0, 1, or 2) of the constant axis, or `None` if no axis is constant.
231    #[inline]
232    pub fn constant_axis(&self) -> Option<usize> {
233        let (lo, hi) = self.bounds();
234        (0..3).find(|&d| lo[d] == hi[d])
235    }
236
237    /// Returns the sorted (ascending) pair of face dimension spans.
238    /// For a face with one constant axis, two spans are non-zero.
239    /// Uses absolute differences so reversal of il/ih, jl/jh, kl/kh is handled.
240    /// The constant axis does not need to match between paired faces
241    /// (e.g. a constant-i face can match a constant-k face).
242    pub fn face_dims(&self) -> (usize, usize) {
243        let mut spans = [
244            self.il.abs_diff(self.ih),
245            self.jl.abs_diff(self.jh),
246            self.kl.abs_diff(self.kh),
247        ];
248        spans.sort();
249        (spans[1], spans[2])
250    }
251
252    /// Compute and fill in the physical direction metadata by sampling the block.
253    ///
254    /// For a face with one constant axis (e.g. K-constant), the two varying axes
255    /// form u and v. We sample the block at the min and max corners of each
256    /// varying axis to determine which physical axis (x, y, z) it primarily
257    /// aligns with and whether it is increasing.
258    pub fn compute_direction(&mut self, block: &Block) {
259        // Determine which axis is constant (use normalized min/max)
260        let i_const = self.i_lo() == self.i_hi();
261        let j_const = self.j_lo() == self.j_hi();
262        let k_const = self.k_lo() == self.k_hi();
263
264        let (ilo, jlo, klo) = (self.i_lo(), self.j_lo(), self.k_lo());
265        let (ihi, jhi, khi) = (self.i_hi(), self.j_hi(), self.k_hi());
266
267        // Identify u and v varying axes
268        // Convention: for K-const → u=I, v=J; for J-const → u=I, v=K; for I-const → u=J, v=K
269        let (u_min_ijk, u_max_ijk, v_min_ijk, v_max_ijk) = if k_const || !i_const && !j_const {
270            // K-constant (or all varying, default to K-const convention)
271            (
272                (ilo, jlo, klo),
273                (ihi, jlo, klo),
274                (ilo, jlo, klo),
275                (ilo, jhi, klo),
276            )
277        } else if j_const {
278            (
279                (ilo, jlo, klo),
280                (ihi, jlo, klo),
281                (ilo, jlo, klo),
282                (ilo, jlo, khi),
283            )
284        } else {
285            // I-constant
286            (
287                (ilo, jlo, klo),
288                (ilo, jhi, klo),
289                (ilo, jlo, klo),
290                (ilo, jlo, khi),
291            )
292        };
293
294        // Sample block coordinates
295        let (ux0, uy0, uz0) = block.xyz(u_min_ijk.0, u_min_ijk.1, u_min_ijk.2);
296        let (ux1, uy1, uz1) = block.xyz(u_max_ijk.0, u_max_ijk.1, u_max_ijk.2);
297        let (vx0, vy0, vz0) = block.xyz(v_min_ijk.0, v_min_ijk.1, v_min_ijk.2);
298        let (vx1, vy1, vz1) = block.xyz(v_max_ijk.0, v_max_ijk.1, v_max_ijk.2);
299
300        // Determine dominant physical axis for u
301        let du = [(ux1 - ux0), (uy1 - uy0), (uz1 - uz0)];
302        let abs_du = [du[0].abs(), du[1].abs(), du[2].abs()];
303        let u_axis_idx = if abs_du[0] >= abs_du[1] && abs_du[0] >= abs_du[2] {
304            0
305        } else if abs_du[1] >= abs_du[2] {
306            1
307        } else {
308            2
309        };
310        let u_axis = ['x', 'y', 'z'][u_axis_idx];
311        let u_increasing = du[u_axis_idx] >= 0.0;
312
313        // Determine dominant physical axis for v
314        let dv = [(vx1 - vx0), (vy1 - vy0), (vz1 - vz0)];
315        let abs_dv = [dv[0].abs(), dv[1].abs(), dv[2].abs()];
316        let v_axis_idx = if abs_dv[0] >= abs_dv[1] && abs_dv[0] >= abs_dv[2] {
317            0
318        } else if abs_dv[1] >= abs_dv[2] {
319            1
320        } else {
321            2
322        };
323        let v_axis = ['x', 'y', 'z'][v_axis_idx];
324        let v_increasing = dv[v_axis_idx] >= 0.0;
325
326        self.u_physical = Some((u_axis, u_increasing));
327        self.v_physical = Some((v_axis, v_increasing));
328    }
329
330    /// Scale the index ranges by `factor`.
331    pub fn scale_indices(&mut self, factor: usize) {
332        if factor <= 1 {
333            return;
334        }
335        self.il *= factor;
336        self.jl *= factor;
337        self.kl *= factor;
338        self.ih *= factor;
339        self.jh *= factor;
340        self.kh *= factor;
341    }
342
343    /// Reduce the index ranges by `divisor`.
344    pub fn divide_indices(&mut self, divisor: usize) {
345        if divisor <= 1 {
346            return;
347        }
348        self.il /= divisor;
349        self.jl /= divisor;
350        self.kl /= divisor;
351        self.ih /= divisor;
352        self.jh /= divisor;
353        self.kh /= divisor;
354    }
355
356    /// Build a compact key tuple for set/map lookups.
357    #[inline]
358    pub fn index_key(&self) -> FaceKey {
359        (
360            self.block_index,
361            self.il,
362            self.jl,
363            self.kl,
364            self.ih,
365            self.jh,
366            self.kh,
367        )
368    }
369
370    /// Reconstruct a Face from this record using the provided blocks.
371    ///
372    /// Uses normalized `i_lo()/i_hi()` values to ensure valid face geometry.
373    pub fn to_face(&self, blocks: &[Block]) -> Option<Face> {
374        let block = blocks.get(self.block_index)?;
375        let mut face = crate::block_face_functions::create_face_from_diagonals(
376            block,
377            self.i_lo(),
378            self.j_lo(),
379            self.k_lo(),
380            self.i_hi(),
381            self.j_hi(),
382            self.k_hi(),
383        );
384        face.set_block_index(self.block_index);
385        if let Some(id) = self.id {
386            face.set_id(id);
387        }
388        Some(face)
389    }
390}
391
392/// Helper trait to print summaries of face records.
393pub trait FaceRecordTraits {
394    fn print(&self);
395}
396
397impl FaceRecordTraits for [FaceRecord] {
398    fn print(&self) {
399        for face in self {
400            println!(
401                "face block{} id {:?}: [{},{},{} → {},{},{}]",
402                face.block_index, face.id, face.il, face.jl, face.kl, face.ih, face.jh, face.kh
403            );
404        }
405    }
406}
407
408impl FaceRecordTraits for Vec<FaceRecord> {
409    fn print(&self) {
410        self.as_slice().print();
411    }
412}
413
414/// The 8 canonical 2x2 permutation matrices for face orientation.
415///
416/// Each matrix operates on parametric (u, v) coordinates. The index encodes:
417/// - bit 0: `u_reversed`
418/// - bit 1: `v_reversed`
419/// - bit 2: `swapped` (transpose u and v)
420///
421/// The index is computed as:
422/// ```text
423/// index = u_reversed | (v_reversed << 1) | (swapped << 2)
424/// ```
425///
426/// | Index | Matrix              | Effect            |
427/// |:-----:|:-------------------:|:-----------------:|
428/// |   0   | `[[ 1, 0],[ 0, 1]]`| identity          |
429/// |   1   | `[[-1, 0],[ 0, 1]]`| flip u            |
430/// |   2   | `[[ 1, 0],[ 0,-1]]`| flip v            |
431/// |   3   | `[[-1, 0],[ 0,-1]]`| flip both         |
432/// |   4   | `[[ 0, 1],[ 1, 0]]`| transpose         |
433/// |   5   | `[[ 0,-1],[ 1, 0]]`| transpose + flip u|
434/// |   6   | `[[ 0, 1],[-1, 0]]`| transpose + flip v|
435/// |   7   | `[[ 0,-1],[-1, 0]]`| transpose + both  |
436///
437/// # Examples
438///
439/// ```
440/// use plot3d::PERMUTATION_MATRICES;
441///
442/// // Identity (index 0): no reversal, no swap
443/// assert_eq!(PERMUTATION_MATRICES[0], [[1, 0], [0, 1]]);
444///
445/// // Index 5 = u_reversed (bit 0) + swapped (bit 2) = 1 + 4
446/// assert_eq!(PERMUTATION_MATRICES[5], [[0, -1], [1, 0]]);
447///
448/// // Verify the full table has exactly 8 entries
449/// assert_eq!(PERMUTATION_MATRICES.len(), 8);
450/// ```
451pub const PERMUTATION_MATRICES: [[[i8; 2]; 2]; 8] = [
452    [[1, 0], [0, 1]],   // 0: identity
453    [[-1, 0], [0, 1]],  // 1: u reversed
454    [[1, 0], [0, -1]],  // 2: v reversed
455    [[-1, 0], [0, -1]], // 3: both reversed
456    [[0, 1], [1, 0]],   // 4: swapped
457    [[0, -1], [1, 0]],  // 5: swap + u reversed
458    [[0, 1], [-1, 0]],  // 6: swap + v reversed
459    [[0, -1], [-1, 0]], // 7: swap + both reversed
460];
461
462/// Whether a face match is in-plane or cross-plane.
463///
464/// When two block faces share an interface, their constant axes may or may
465/// not be the same. This distinction matters because cross-plane matches
466/// require a parametric axis swap (bit 2 of `permutation_index`), while
467/// in-plane matches only need reversal flags.
468///
469/// - [`InPlane`](OrientationPlane::InPlane): both faces have the same
470///   constant axis (e.g., both K-constant). Only the 4 non-swap
471///   permutations (indices 0-3) apply.
472/// - [`CrossPlane`](OrientationPlane::CrossPlane): faces have different
473///   constant axes (e.g., K-constant abutting J-constant). The full set of
474///   8 permutations (indices 0-7) must be tested.
475///
476/// # Examples
477///
478/// ```
479/// use plot3d::OrientationPlane;
480///
481/// let plane = OrientationPlane::InPlane;
482/// assert_eq!(plane, OrientationPlane::InPlane);
483///
484/// let cross = OrientationPlane::CrossPlane;
485/// assert_ne!(plane, cross);
486/// ```
487#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
488#[serde(rename_all = "kebab-case")]
489pub enum OrientationPlane {
490    InPlane,
491    CrossPlane,
492}
493
494/// Describes the parametric orientation between two matched faces using a
495/// permutation matrix index (0-7).
496///
497/// The permutation matrix transforms face2's parametric (u, v) coordinates
498/// to align with face1's. The `plane` field indicates whether the faces share
499/// the same constant axis (in-plane) or have different constant axes
500/// (cross-plane).
501///
502/// Construct via [`Orientation::from_flags`] when you have individual boolean
503/// flags, or set `permutation_index` directly when you already know the
504/// encoded value.
505///
506/// # Bit layout
507///
508/// ```text
509/// permutation_index = u_reversed | (v_reversed << 1) | (swapped << 2)
510/// ```
511///
512/// # Examples
513///
514/// ```
515/// use plot3d::{Orientation, OrientationPlane, PERMUTATION_MATRICES};
516///
517/// // Build from boolean flags: u reversed, v not reversed, axes swapped
518/// let orient = Orientation::from_flags(true, false, true, OrientationPlane::CrossPlane);
519/// assert_eq!(orient.permutation_index, 5); // 1 + 0 + 4
520/// assert!(orient.u_reversed());
521/// assert!(!orient.v_reversed());
522/// assert!(orient.swapped());
523///
524/// // Retrieve the 2x2 matrix
525/// let m = orient.matrix();
526/// assert_eq!(*m, PERMUTATION_MATRICES[5]);
527/// ```
528#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
529pub struct Orientation {
530    /// Index (0-7) into [`PERMUTATION_MATRICES`].
531    pub permutation_index: u8,
532    /// Whether this is an in-plane or cross-plane match.
533    pub plane: OrientationPlane,
534}
535
536impl Orientation {
537    /// Construct from the legacy boolean flags.
538    pub fn from_flags(
539        u_reversed: bool,
540        v_reversed: bool,
541        swapped: bool,
542        plane: OrientationPlane,
543    ) -> Self {
544        let index = (u_reversed as u8) | ((v_reversed as u8) << 1) | ((swapped as u8) << 2);
545        Self {
546            permutation_index: index,
547            plane,
548        }
549    }
550
551    /// Whether block2's u-axis is reversed relative to block1's.
552    pub fn u_reversed(&self) -> bool {
553        self.permutation_index & 1 != 0
554    }
555
556    /// Whether block2's v-axis is reversed relative to block1's.
557    pub fn v_reversed(&self) -> bool {
558        self.permutation_index & 2 != 0
559    }
560
561    /// Whether block2's u and v axes are transposed relative to block1's.
562    pub fn swapped(&self) -> bool {
563        self.permutation_index & 4 != 0
564    }
565
566    /// Get the 2×2 permutation matrix for this orientation.
567    pub fn matrix(&self) -> &[[i8; 2]; 2] {
568        &PERMUTATION_MATRICES[self.permutation_index as usize]
569    }
570}
571
572/// Aggregates the matching data between two faces.
573///
574/// Each entry stores the corner ranges (on both blocks) and every coincident
575/// node that was found for that interface.
576#[derive(Clone, Debug, Serialize)]
577pub struct FaceMatch {
578    pub block1: FaceRecord,
579    pub block2: FaceRecord,
580    pub points: Vec<MatchPoint>,
581    /// Orientation relationship between block1 and block2 faces.
582    /// `None` for legacy code paths or partial matches where orientation
583    /// was not detected.
584    #[serde(default)]
585    pub orientation: Option<Orientation>,
586}
587
588impl FaceMatch {
589    /// Downscale both participating face records by `divisor`.
590    /// Note: MatchPoints are NOT scaled — they may be from full-resolution
591    /// Phase 2/3 matching and should only be used with full-resolution blocks.
592    pub fn divide_indices(&mut self, divisor: usize) {
593        self.block1.divide_indices(divisor);
594        self.block2.divide_indices(divisor);
595    }
596
597    /// Upscale both participating face records by `factor`.
598    pub fn scale_indices(&mut self, factor: usize) {
599        self.block1.scale_indices(factor);
600        self.block2.scale_indices(factor);
601    }
602}
603
604/// Helper trait to print summaries of face matches.
605pub trait FaceMatchPrinter {
606    fn print(&self);
607}
608
609impl FaceMatchPrinter for [FaceMatch] {
610    fn print(&self) {
611        for (idx, m) in self.iter().enumerate() {
612            let block1 = &m.block1;
613            let block2 = &m.block2;
614            let node_count = m.points.len();
615            let node_label = if node_count == 1 { "node" } else { "nodes" };
616            println!(
617                "match #{idx}: block{block1_idx:02} [{il1:03},{jl1:03},{kl1:03} -> {ih1:03},{jh1:03},{kh1:03}] <-> block{block2_idx:02} [{il2:03},{jl2:03},{kl2:03} -> {ih2:03},{jh2:03},{kh2:03}] ({node_count} {node_label})",
618                block1_idx = block1.block_index,
619                il1 = block1.il,
620                jl1 = block1.jl,
621                kl1 = block1.kl,
622                ih1 = block1.ih,
623                jh1 = block1.jh,
624                kh1 = block1.kh,
625                block2_idx = block2.block_index,
626                il2 = block2.il,
627                jl2 = block2.jl,
628                kl2 = block2.kl,
629                ih2 = block2.ih,
630                jh2 = block2.jh,
631                kh2 = block2.kh,
632                node_count = node_count,
633                node_label = node_label,
634            );
635        }
636    }
637}
638
639impl FaceMatchPrinter for Vec<FaceMatch> {
640    fn print(&self) {
641        self.as_slice().print();
642    }
643}
644
645/// Semantic alias for a periodic face pair (same structure as [`FaceMatch`]).
646pub type PeriodicPair = FaceMatch;