plot3d/serialization.rs
1//! JSON serialization for face records and face matches.
2//!
3//! Provides two output formats controlled by the `--diagonal` flag:
4//!
5//! # Default format (lo/hi)
6//!
7//! Face bounds use ascending `lo`/`hi` keys. Every match includes a
8//! `permutation_index` (0-7) indicating which [`PERMUTATION_MATRICES`]
9//! entry transforms face B to match face A.
10//!
11//! ```json
12//! {
13//! "block1": { "block_index": 0, "lo": [0,0,0], "hi": [0,101,33] },
14//! "block2": { "block_index": 30, "lo": [0,0,0], "hi": [0,101,33] },
15//! "permutation_index": 3
16//! }
17//! ```
18//!
19//! # Diagonal format (`--diagonal`)
20//!
21//! - **In-plane** matches (perm 0-3): block2's `lb`/`ub` encodes traversal
22//! direction. `permutation_index: -1` (direction is fully in the bounds).
23//! - **Cross-plane** matches (perm 4-7): ascending `lb`/`ub` bounds with the
24//! actual `permutation_index`, since lb/ub can't encode a swap.
25//!
26//! ```json
27//! {
28//! "block1": { "block_index": 0, "lb": [0,0,0], "ub": [0,101,33] },
29//! "block2": { "block_index": 30, "lb": [0,101,33], "ub": [0,0,0] },
30//! "permutation_index": -1
31//! }
32//! ```
33//!
34//! [`PERMUTATION_MATRICES`]: crate::face_record::PERMUTATION_MATRICES
35
36use crate::face_record::{FaceMatch, FaceRecord, OrientationPlane, PERMUTATION_MATRICES};
37use serde_json::{json, Value};
38
39// ── Default format (lo/hi) ──────────────────────────────────────────────
40
41/// Convert a [`FaceRecord`] to JSON with ascending `lo`/`hi` bounds.
42pub fn face_record_to_json(rec: &FaceRecord) -> Value {
43 let (lo, hi) = rec.bounds();
44 let mut obj = json!({
45 "block_index": rec.block_index,
46 "lo": lo,
47 "hi": hi,
48 });
49 if let Some(id) = rec.id {
50 obj["id"] = json!(id);
51 }
52 obj
53}
54
55/// Convert a [`FaceMatch`] to JSON (`lo`/`hi` + `permutation_index` 0-7).
56pub fn face_match_to_json(fm: &FaceMatch) -> Value {
57 let perm_idx: i8 = fm
58 .orientation
59 .as_ref()
60 .map(|o| o.permutation_index as i8)
61 .unwrap_or(0);
62 json!({
63 "block1": face_record_to_json(&fm.block1),
64 "block2": face_record_to_json(&fm.block2),
65 "permutation_index": perm_idx,
66 })
67}
68
69// ── Diagonal format (lb/ub) ─────────────────────────────────────────────
70
71/// Convert a [`FaceRecord`] to JSON with ascending `lb`/`ub` bounds.
72pub fn face_record_to_diagonal_json(rec: &FaceRecord) -> Value {
73 let (lo, hi) = rec.bounds();
74 let mut obj = json!({
75 "block_index": rec.block_index,
76 "lb": lo,
77 "ub": hi,
78 });
79 if let Some(id) = rec.id {
80 obj["id"] = json!(id);
81 }
82 obj
83}
84
85/// Convert a [`FaceRecord`] to JSON with directional `lb`/`ub` based on permutation.
86///
87/// Reconstructs traversal direction from `permutation_index` bits:
88/// - bit 0 (`u_reversed`): reverse the first varying axis
89/// - bit 1 (`v_reversed`): reverse the second varying axis
90fn face_record_to_directed_diagonal_json(rec: &FaceRecord, perm_idx: u8) -> Value {
91 let (lo, hi) = rec.bounds();
92 let const_ax = rec.constant_axis();
93
94 let (lb, ub) = match const_ax {
95 Some(c) => {
96 let vary: Vec<usize> = (0..3).filter(|&d| d != c).collect();
97 let d0 = vary[0]; // u axis
98 let d1 = vary[1]; // v axis
99 let u_rev = perm_idx & 1 != 0;
100 let v_rev = perm_idx & 2 != 0;
101
102 let mut lb = lo;
103 let mut ub = hi;
104 if u_rev {
105 lb[d0] = hi[d0];
106 ub[d0] = lo[d0];
107 }
108 if v_rev {
109 lb[d1] = hi[d1];
110 ub[d1] = lo[d1];
111 }
112 (lb, ub)
113 }
114 None => (lo, hi),
115 };
116
117 let mut obj = json!({
118 "block_index": rec.block_index,
119 "lb": lb,
120 "ub": ub,
121 });
122 if let Some(id) = rec.id {
123 obj["id"] = json!(id);
124 }
125 obj
126}
127
128/// Convert a [`FaceMatch`] to diagonal JSON format.
129///
130/// - **In-plane** (perm 0-3): block2's `lb`/`ub` encodes direction. `permutation_index: -1`.
131/// - **Cross-plane** (perm 4-7): ascending `lb`/`ub`. `permutation_index: N` (actual index).
132pub fn face_match_to_diagonal_json(fm: &FaceMatch) -> Value {
133 let orient = fm.orientation.as_ref();
134 let perm_idx = orient.map(|o| o.permutation_index).unwrap_or(0);
135 let is_cross_plane = orient
136 .map(|o| o.plane == OrientationPlane::CrossPlane)
137 .unwrap_or(false);
138
139 if is_cross_plane {
140 // Cross-plane: lb/ub can't encode swap → ascending bounds + actual permutation_index
141 json!({
142 "block1": face_record_to_diagonal_json(&fm.block1),
143 "block2": face_record_to_diagonal_json(&fm.block2),
144 "permutation_index": perm_idx,
145 })
146 } else {
147 // In-plane: encode direction in block2's lb/ub, permutation_index = -1
148 json!({
149 "block1": face_record_to_diagonal_json(&fm.block1),
150 "block2": face_record_to_directed_diagonal_json(&fm.block2, perm_idx),
151 "permutation_index": -1,
152 })
153 }
154}
155
156/// Serialize the 8 permutation matrices as a JSON array (for inclusion in output headers).
157pub fn permutation_matrices_json() -> Vec<Value> {
158 PERMUTATION_MATRICES
159 .iter()
160 .map(|m| json!([[m[0][0], m[0][1]], [m[1][0], m[1][1]]]))
161 .collect()
162}