1use std::collections::HashSet;
5
6use crate::{
7 block::Block,
8 block_face_functions::{Face, FaceAxis},
9 cylindrical::{to_radius, to_theta},
10 face_record::{FaceKey, FaceRecord},
11 utils::{apply_rotation, distance3},
12 Float, PI,
13};
14
15#[inline]
17pub(crate) fn axial_coord(p: [Float; 3], rotation_axis: char) -> Float {
18 match rotation_axis.to_ascii_lowercase() {
19 'x' => p[0],
20 'y' => p[1],
21 _ => p[2],
22 }
23}
24
25#[derive(Clone, Debug)]
27#[allow(dead_code)]
28pub(crate) struct CylindricalFaceInfo {
29 pub theta_centroid: Float,
30 pub axial_centroid: Float,
31 pub radial_centroid: Float,
32 pub theta_min: Float,
33 pub theta_max: Float,
34 pub axial_min: Float,
35 pub axial_max: Float,
36 pub radial_min: Float,
37 pub radial_max: Float,
38}
39
40impl CylindricalFaceInfo {
41 pub fn from_face(face: &Face, rotation_axis: char) -> Self {
42 let verts = face.vertices();
43 let c = face.centroid();
44 let theta_c = to_theta(c[0], c[1], c[2], rotation_axis);
45 let axial_c = axial_coord(c, rotation_axis);
46 let radial_c = to_radius(c[0], c[1], c[2], rotation_axis);
47
48 let mut theta_min = theta_c;
49 let mut theta_max = theta_c;
50 let mut axial_min = axial_c;
51 let mut axial_max = axial_c;
52 let mut radial_min = radial_c;
53 let mut radial_max = radial_c;
54
55 for v in verts {
56 let th = to_theta(v[0], v[1], v[2], rotation_axis);
57 let ax = axial_coord(*v, rotation_axis);
58 let r = to_radius(v[0], v[1], v[2], rotation_axis);
59 theta_min = theta_min.min(th);
60 theta_max = theta_max.max(th);
61 axial_min = axial_min.min(ax);
62 axial_max = axial_max.max(ax);
63 radial_min = radial_min.min(r);
64 radial_max = radial_max.max(r);
65 }
66
67 Self {
68 theta_centroid: theta_c,
69 axial_centroid: axial_c,
70 radial_centroid: radial_c,
71 theta_min,
72 theta_max,
73 axial_min,
74 axial_max,
75 radial_min,
76 radial_max,
77 }
78 }
79}
80
81#[derive(Clone, Debug)]
83pub(crate) struct StructuredEdge {
84 pub coords: Vec<[Float; 3]>,
85}
86
87#[derive(Debug)]
89pub(crate) enum EdgeMatchResult {
90 None,
91 Full,
92 Partial,
93}
94
95pub(crate) struct FacePool {
97 pub faces: Vec<Face>,
98 cyl_info: Vec<CylindricalFaceInfo>,
99 theta_sorted: Vec<usize>,
101 consumed: HashSet<FaceKey>,
102 rotation_axis: char,
103}
104
105impl FacePool {
106 pub fn new(faces: Vec<Face>, rotation_axis: char) -> Self {
107 let cyl_info: Vec<CylindricalFaceInfo> = faces
108 .iter()
109 .map(|f| CylindricalFaceInfo::from_face(f, rotation_axis))
110 .collect();
111 let mut theta_sorted: Vec<usize> = (0..faces.len()).collect();
112 theta_sorted.sort_by(|&a, &b| {
113 cyl_info[a]
114 .theta_centroid
115 .partial_cmp(&cyl_info[b].theta_centroid)
116 .unwrap_or(std::cmp::Ordering::Equal)
117 });
118 Self {
119 faces,
120 cyl_info,
121 theta_sorted,
122 consumed: HashSet::new(),
123 rotation_axis,
124 }
125 }
126
127 pub fn add_face(&mut self, face: Face) {
128 let info = CylindricalFaceInfo::from_face(&face, self.rotation_axis);
129 let idx = self.faces.len();
130 self.faces.push(face);
131 self.cyl_info.push(info);
132 let theta = self.cyl_info[idx].theta_centroid;
134 let pos = self
135 .theta_sorted
136 .binary_search_by(|&i| {
137 self.cyl_info[i]
138 .theta_centroid
139 .partial_cmp(&theta)
140 .unwrap_or(std::cmp::Ordering::Equal)
141 })
142 .unwrap_or_else(|p| p);
143 self.theta_sorted.insert(pos, idx);
144 }
145
146 pub fn consume(&mut self, key: FaceKey) {
147 self.consumed.insert(key);
148 }
149
150 pub fn is_consumed(&self, idx: usize) -> bool {
151 let face = &self.faces[idx];
152 self.consumed.contains(&face.index_key())
153 }
154
155 pub fn find_candidates(
158 &self,
159 target_theta: Float,
160 axial_range: (Float, Float),
161 radial_range: (Float, Float),
162 theta_tol: Float,
163 ) -> Vec<usize> {
164 let theta_lo = target_theta - theta_tol;
165 let theta_hi = target_theta + theta_tol;
166
167 let mut candidates = Vec::new();
168
169 let start = self
171 .theta_sorted
172 .binary_search_by(|&i| {
173 self.cyl_info[i]
174 .theta_centroid
175 .partial_cmp(&theta_lo)
176 .unwrap_or(std::cmp::Ordering::Equal)
177 })
178 .unwrap_or_else(|p| p);
179
180 for &pool_idx in &self.theta_sorted[start..] {
182 let info = &self.cyl_info[pool_idx];
183 if info.theta_centroid > theta_hi {
184 break;
185 }
186 if self.is_consumed(pool_idx) {
187 continue;
188 }
189 if info.axial_max
191 < axial_range.0 - 0.1 * (axial_range.1 - axial_range.0).abs().max(1e-12)
192 || info.axial_min
193 > axial_range.1 + 0.1 * (axial_range.1 - axial_range.0).abs().max(1e-12)
194 {
195 continue;
196 }
197 if info.radial_max
199 < radial_range.0 - 0.1 * (radial_range.1 - radial_range.0).abs().max(1e-12)
200 || info.radial_min
201 > radial_range.1 + 0.1 * (radial_range.1 - radial_range.0).abs().max(1e-12)
202 {
203 continue;
204 }
205 candidates.push(pool_idx);
206 }
207
208 if theta_lo < -PI {
211 let wrapped_lo = theta_lo + 2.0 * PI;
212 let wrapped_hi = PI; let ws = self
214 .theta_sorted
215 .binary_search_by(|&i| {
216 self.cyl_info[i]
217 .theta_centroid
218 .partial_cmp(&wrapped_lo)
219 .unwrap_or(std::cmp::Ordering::Equal)
220 })
221 .unwrap_or_else(|p| p);
222 for &pool_idx in &self.theta_sorted[ws..] {
223 let info = &self.cyl_info[pool_idx];
224 if info.theta_centroid > wrapped_hi {
225 break;
226 }
227 if self.is_consumed(pool_idx) {
228 continue;
229 }
230 if info.axial_max
231 < axial_range.0 - 0.1 * (axial_range.1 - axial_range.0).abs().max(1e-12)
232 || info.axial_min
233 > axial_range.1 + 0.1 * (axial_range.1 - axial_range.0).abs().max(1e-12)
234 {
235 continue;
236 }
237 if info.radial_max
238 < radial_range.0 - 0.1 * (radial_range.1 - radial_range.0).abs().max(1e-12)
239 || info.radial_min
240 > radial_range.1 + 0.1 * (radial_range.1 - radial_range.0).abs().max(1e-12)
241 {
242 continue;
243 }
244 candidates.push(pool_idx);
245 }
246 }
247 if theta_hi > PI {
249 let wrapped_hi = theta_hi - 2.0 * PI;
250 let wrapped_lo = -PI;
251 let ws = self
252 .theta_sorted
253 .binary_search_by(|&i| {
254 self.cyl_info[i]
255 .theta_centroid
256 .partial_cmp(&wrapped_lo)
257 .unwrap_or(std::cmp::Ordering::Equal)
258 })
259 .unwrap_or_else(|p| p);
260 for &pool_idx in &self.theta_sorted[ws..] {
261 let info = &self.cyl_info[pool_idx];
262 if info.theta_centroid > wrapped_hi {
263 break;
264 }
265 if self.is_consumed(pool_idx) {
266 continue;
267 }
268 if info.axial_max
269 < axial_range.0 - 0.1 * (axial_range.1 - axial_range.0).abs().max(1e-12)
270 || info.axial_min
271 > axial_range.1 + 0.1 * (axial_range.1 - axial_range.0).abs().max(1e-12)
272 {
273 continue;
274 }
275 if info.radial_max
276 < radial_range.0 - 0.1 * (radial_range.1 - radial_range.0).abs().max(1e-12)
277 || info.radial_min
278 > radial_range.1 + 0.1 * (radial_range.1 - radial_range.0).abs().max(1e-12)
279 {
280 continue;
281 }
282 candidates.push(pool_idx);
283 }
284 }
285
286 candidates
287 }
288
289 pub fn find_rotational_candidates(
292 &self,
293 idx: usize,
294 rotation_angle: Float,
295 theta_tol: Float,
296 ) -> Vec<usize> {
297 let info = &self.cyl_info[idx];
298 let target_fwd = info.theta_centroid + rotation_angle;
299 let target_rev = info.theta_centroid - rotation_angle;
300 let axial_range = (info.axial_min, info.axial_max);
301 let radial_range = (info.radial_min, info.radial_max);
302 let mut candidates = self.find_candidates(target_fwd, axial_range, radial_range, theta_tol);
303 candidates.extend(self.find_candidates(target_rev, axial_range, radial_range, theta_tol));
304 candidates.sort_unstable();
305 candidates.dedup();
306 candidates
307 }
308
309 pub fn active_indices(&self) -> Vec<usize> {
311 (0..self.faces.len())
312 .filter(|&i| !self.is_consumed(i))
313 .collect()
314 }
315
316 pub fn drain_as_records(&self) -> Vec<FaceRecord> {
318 self.faces
319 .iter()
320 .enumerate()
321 .filter(|(i, _)| !self.is_consumed(*i))
322 .map(|(_, f)| f.to_record())
323 .collect()
324 }
325}
326
327pub(crate) fn extract_face_edges(face: &Face, block: &Block) -> Vec<StructuredEdge> {
333 let axis = match face.const_axis() {
334 Some(a) => a,
335 None => return Vec::new(),
336 };
337 let mut edges = Vec::with_capacity(4);
338
339 match axis {
340 FaceAxis::I => {
341 let ic = face.imin();
342 edges.push(build_edge(
343 block,
344 (face.kmin()..=face.kmax())
345 .map(|k| (ic, face.jmin(), k))
346 .collect(),
347 ));
348 edges.push(build_edge(
349 block,
350 (face.kmin()..=face.kmax())
351 .map(|k| (ic, face.jmax(), k))
352 .collect(),
353 ));
354 edges.push(build_edge(
355 block,
356 (face.jmin()..=face.jmax())
357 .map(|j| (ic, j, face.kmin()))
358 .collect(),
359 ));
360 edges.push(build_edge(
361 block,
362 (face.jmin()..=face.jmax())
363 .map(|j| (ic, j, face.kmax()))
364 .collect(),
365 ));
366 }
367 FaceAxis::J => {
368 let jc = face.jmin();
369 edges.push(build_edge(
370 block,
371 (face.kmin()..=face.kmax())
372 .map(|k| (face.imin(), jc, k))
373 .collect(),
374 ));
375 edges.push(build_edge(
376 block,
377 (face.kmin()..=face.kmax())
378 .map(|k| (face.imax(), jc, k))
379 .collect(),
380 ));
381 edges.push(build_edge(
382 block,
383 (face.imin()..=face.imax())
384 .map(|i| (i, jc, face.kmin()))
385 .collect(),
386 ));
387 edges.push(build_edge(
388 block,
389 (face.imin()..=face.imax())
390 .map(|i| (i, jc, face.kmax()))
391 .collect(),
392 ));
393 }
394 FaceAxis::K => {
395 let kc = face.kmin();
396 edges.push(build_edge(
397 block,
398 (face.jmin()..=face.jmax())
399 .map(|j| (face.imin(), j, kc))
400 .collect(),
401 ));
402 edges.push(build_edge(
403 block,
404 (face.jmin()..=face.jmax())
405 .map(|j| (face.imax(), j, kc))
406 .collect(),
407 ));
408 edges.push(build_edge(
409 block,
410 (face.imin()..=face.imax())
411 .map(|i| (i, face.jmin(), kc))
412 .collect(),
413 ));
414 edges.push(build_edge(
415 block,
416 (face.imin()..=face.imax())
417 .map(|i| (i, face.jmax(), kc))
418 .collect(),
419 ));
420 }
421 }
422 edges
423}
424
425fn build_edge(block: &Block, ijk_list: Vec<(usize, usize, usize)>) -> StructuredEdge {
426 let coords: Vec<[Float; 3]> = ijk_list
427 .iter()
428 .map(|&(i, j, k)| {
429 let (x, y, z) = block.xyz(i, j, k);
430 [x, y, z]
431 })
432 .collect();
433 StructuredEdge { coords }
434}
435
436pub(crate) fn match_edges(
439 edge_a: &StructuredEdge,
440 edge_b: &StructuredEdge,
441 rotation_matrix: [[Float; 3]; 3],
442 tol: Float,
443) -> EdgeMatchResult {
444 if edge_a.coords.is_empty() || edge_b.coords.is_empty() {
445 return EdgeMatchResult::None;
446 }
447
448 let rotated_a: Vec<[Float; 3]> = edge_a
449 .coords
450 .iter()
451 .map(|p| apply_rotation(*p, rotation_matrix))
452 .collect();
453
454 let a_matched = rotated_a
457 .iter()
458 .filter(|ra| edge_b.coords.iter().any(|pb| distance3(**ra, *pb) <= tol))
459 .count();
460 let b_matched = edge_b
461 .coords
462 .iter()
463 .filter(|pb| rotated_a.iter().any(|ra| distance3(*ra, **pb) <= tol))
464 .count();
465
466 let matched = a_matched.min(b_matched);
467
468 if matched < 2 {
469 EdgeMatchResult::None
470 } else if a_matched == rotated_a.len() && b_matched == edge_b.coords.len() {
471 EdgeMatchResult::Full
472 } else {
473 EdgeMatchResult::Partial
474 }
475}
476
477pub(crate) fn count_edge_matches(
479 edges_a: &[StructuredEdge],
480 edges_b: &[StructuredEdge],
481 rotation_matrix: [[Float; 3]; 3],
482 tol: Float,
483) -> usize {
484 let mut count = 0;
485 for ea in edges_a {
486 for eb in edges_b {
487 match match_edges(ea, eb, rotation_matrix, tol) {
488 EdgeMatchResult::None => {}
489 _ => count += 1,
490 }
491 }
492 }
493 count
494}