1use std::collections::{BTreeMap, HashMap};
53
54use energyml_geometry::geometry::{Patch, Topology};
55use energyml_geometry::section::{self, Cut};
56
57use crate::model::{CutShape, SectionTrace, SurfaceCut};
58
59type P = [f64; 2];
60
61#[derive(Clone, Copy, Debug)]
63struct Cell {
64 corners: [u32; 8],
65 column: (u32, u32),
67 layer: u32,
68 bbox: [f64; 4],
70 mean_z: f64,
72}
73
74#[derive(Clone, Debug)]
77pub struct Grid {
78 points: Vec<[f64; 3]>,
79 cells: Vec<Cell>,
80}
81
82impl Grid {
83 pub fn from_patch(patch: &Patch) -> Result<Grid, String> {
86 let Topology::Hexahedra {
87 cells, dimensions, ..
88 } = &patch.topology
89 else {
90 return Err("the grid did not read as hexahedra".to_string());
91 };
92 let [ni, nj, nk] = *dimensions;
93 let per_layer = ni * nj;
94 if per_layer == 0 {
95 return Err(format!("the grid is {ni}x{nj}x{nk}"));
96 }
97 let mut out = Vec::with_capacity(cells.len());
98 for (position, corners) in cells.iter().enumerate() {
99 let Some(number) = patch.topology.cell_at(position) else {
102 continue;
103 };
104 let number = number as usize;
105 let (layer, column) = (number / per_layer, number % per_layer);
106 let at: Vec<[f64; 3]> = corners.iter().map(|c| patch.points[*c as usize]).collect();
107 if at.iter().any(|p| p.iter().any(|v| !v.is_finite())) {
108 continue;
109 }
110 let mut bbox = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
111 for p in &at {
112 bbox = [
113 bbox[0].min(p[0]),
114 bbox[1].min(p[1]),
115 bbox[2].max(p[0]),
116 bbox[3].max(p[1]),
117 ];
118 }
119 out.push(Cell {
120 corners: *corners,
121 column: ((column % ni) as u32, (column / ni) as u32),
122 layer: layer as u32,
123 bbox,
124 mean_z: at.iter().map(|p| p[2]).sum::<f64>() / 8.0,
125 });
126 }
127 if out.is_empty() {
128 return Err("the grid has no defined cell".to_string());
129 }
130 Ok(Grid {
131 points: patch.points.clone(),
132 cells: out,
133 })
134 }
135
136 pub fn cut(&self, sections: &[SectionTrace]) -> Vec<SurfaceCut> {
138 sections.iter().filter_map(|one| self.cut_along(one)).collect()
139 }
140
141 fn cut_along(&self, section: &SectionTrace) -> Option<SurfaceCut> {
142 let trace = §ion.trace;
143 let length: f64 = trace
144 .windows(2)
145 .map(|p| (p[1][0] - p[0][0]).hypot(p[1][1] - p[0][1]))
146 .sum();
147 let tol_s = 1e-6 * length.max(1.0);
151
152 let mut cut_cells: Vec<CellCut> = Vec::new();
153 let mut clipped = 0usize;
154 for cell in &self.cells {
155 if !crosses_box(trace, cell.bbox) {
156 continue;
157 }
158 match self.cut_cell(cell, trace, tol_s) {
159 Tried::Cut(done) => cut_cells.push(done),
160 Tried::Clipped => clipped += 1,
161 Tried::Missed => {}
162 }
163 }
164 if cut_cells.is_empty() {
165 return None;
166 }
167 let z_max = cut_cells
168 .iter()
169 .flat_map(|c| c.top.iter().chain(&c.bottom))
170 .fold(1.0f64, |m, p| m.max(p[1].abs()));
171 let tol_z = 1e-6 * z_max;
172
173 let mut columns: BTreeMap<(u32, u32), Vec<CellCut>> = BTreeMap::new();
176 for done in cut_cells {
177 columns.entry(done.column).or_default().push(done);
178 }
179 let mut pieces: Vec<Piece> = Vec::new();
180 for (_, mut cells) in columns {
181 cells.sort_by(|a, b| a.s_range().0.total_cmp(&b.s_range().0));
182 let mut stretches: Vec<Vec<CellCut>> = Vec::new();
183 let mut reach = f64::NEG_INFINITY;
184 for done in cells {
185 let (from, to) = done.s_range();
186 if stretches.is_empty() || from > reach + tol_s {
187 stretches.push(Vec::new());
188 reach = to;
189 } else {
190 reach = reach.max(to);
191 }
192 stretches
193 .last_mut()
194 .expect("a stretch was just pushed")
195 .push(done);
196 }
197 for stretch in stretches {
198 pieces.push(Piece::of(stretch, tol_z));
199 }
200 }
201 pieces.sort_by(|a, b| a.start().total_cmp(&b.start()));
202
203 let (top, mut faults) = join(pieces.iter().map(|p| &p.top), tol_s, tol_z);
204 let (bottom, floor_steps) = join(pieces.iter().map(|p| &p.bottom), tol_s, tol_z);
205 faults.extend(floor_steps);
206
207 let mut fills: Vec<Vec<P>> = Vec::new();
208 let mut open: HashMap<(u32, u32), usize> = HashMap::new();
209 let mut runs: Vec<Run> = Vec::new();
210 for piece in &pieces {
211 for run in &piece.runs {
212 let joined = open
213 .get(&run.key)
214 .copied()
215 .filter(|at| runs[*at].meets(run, tol_s, tol_z));
216 match joined {
217 Some(at) => runs[at].extend(run),
218 None => {
219 open.insert(run.key, runs.len());
220 runs.push(run.clone());
221 }
222 }
223 }
224 }
225 for run in runs {
226 let mut ring = run.upper;
227 ring.extend(run.lower.into_iter().rev());
228 if ring.len() >= 3 {
229 fills.push(ring);
230 }
231 }
232
233 let worth =
234 |spans: Vec<Vec<P>>| -> Vec<Vec<P>> { spans.into_iter().filter(|s| s.len() >= 2).collect() };
235 let mut notes = Vec::new();
236 if clipped > 0 {
237 notes.push(format!(
238 "{clipped} cell(s) of the grid are met by this section at one face only (a corner of an inclined \
239 cell) and are left out of its envelope"
240 ));
241 }
242 Some(SurfaceCut {
243 section: section.key,
244 shape: CutShape::Envelope {
245 top: worth(top),
246 bottom: worth(bottom),
247 fills,
248 faults,
249 },
250 notes,
251 sources: Vec::new(),
252 })
253 }
254
255 fn cut_cell(&self, cell: &Cell, trace: &[P], tol: f64) -> Tried {
256 let at = |n: usize| self.points[cell.corners[n] as usize];
257 let ring_z = |from: usize| (from..from + 4).map(|n| at(n)[2]).sum::<f64>() / 4.0;
258 let (roof, floor) = if ring_z(0) >= ring_z(4) { (0, 4) } else { (4, 0) };
259 let chain = |from: usize| -> Vec<P> {
260 let local: Vec<[f64; 3]> = (from..from + 4).map(at).collect();
261 let found = section::cut(&local, &[[0, 1, 2], [0, 2, 3]], trace);
262 chain_of(&found, tol)
263 };
264 let (top, bottom) = (chain(roof), chain(floor));
265 match (top.is_empty(), bottom.is_empty()) {
266 (true, true) => Tried::Missed,
267 (false, false) => Tried::Cut(CellCut {
268 column: cell.column,
269 layer: cell.layer,
270 mean_z: cell.mean_z,
271 top,
272 bottom,
273 }),
274 _ => Tried::Clipped,
275 }
276 }
277}
278
279enum Tried {
280 Cut(CellCut),
281 Clipped,
282 Missed,
283}
284
285#[derive(Clone, Debug)]
287struct CellCut {
288 column: (u32, u32),
289 layer: u32,
290 mean_z: f64,
291 top: Vec<P>,
292 bottom: Vec<P>,
293}
294
295impl CellCut {
296 fn s_range(&self) -> (f64, f64) {
297 let ends = [
298 self.top.first(),
299 self.top.last(),
300 self.bottom.first(),
301 self.bottom.last(),
302 ];
303 let s = ends.iter().flatten().map(|p| p[0]);
304 (
305 s.clone().fold(f64::INFINITY, f64::min),
306 s.fold(f64::NEG_INFINITY, f64::max),
307 )
308 }
309}
310
311#[derive(Clone, Debug)]
314struct Piece {
315 top: Vec<P>,
316 bottom: Vec<P>,
317 runs: Vec<Run>,
318}
319
320impl Piece {
321 fn start(&self) -> f64 {
322 self.top[0][0].min(self.bottom[0][0])
323 }
324
325 fn of(mut cells: Vec<CellCut>, tol_z: f64) -> Piece {
328 cells.sort_by(|a, b| b.mean_z.total_cmp(&a.mean_z));
329 let mut runs: Vec<Run> = Vec::new();
330 let mut last: Option<&CellCut> = None;
331 for cell in &cells {
332 let continues = last.is_some_and(|prev| {
333 prev.layer.abs_diff(cell.layer) == 1 && touches(&prev.bottom, &cell.top, tol_z)
334 });
335 if continues {
336 let run = runs.last_mut().expect("a run is open");
337 run.lower = cell.bottom.clone();
338 run.key = (run.key.0.min(cell.layer), run.key.1.max(cell.layer));
339 } else {
340 runs.push(Run {
341 key: (cell.layer, cell.layer),
342 upper: cell.top.clone(),
343 lower: cell.bottom.clone(),
344 });
345 }
346 last = Some(cell);
347 }
348 let highest = cells.first().expect("a stretch has a cell");
349 let lowest = cells.last().expect("a stretch has a cell");
350 Piece {
351 top: highest.top.clone(),
352 bottom: lowest.bottom.clone(),
353 runs,
354 }
355 }
356}
357
358#[derive(Clone, Debug)]
361struct Run {
362 key: (u32, u32),
364 upper: Vec<P>,
365 lower: Vec<P>,
366}
367
368impl Run {
369 fn meets(&self, next: &Run, tol_s: f64, tol_z: f64) -> bool {
370 let same = |a: Option<&P>, b: Option<&P>| match (a, b) {
371 (Some(a), Some(b)) => (a[0] - b[0]).abs() <= tol_s && (a[1] - b[1]).abs() <= tol_z,
372 _ => false,
373 };
374 same(self.upper.last(), next.upper.first()) && same(self.lower.last(), next.lower.first())
375 }
376
377 fn extend(&mut self, next: &Run) {
378 self.upper.extend(next.upper.iter().skip(1));
379 self.lower.extend(next.lower.iter().skip(1));
380 }
381}
382
383fn touches(above: &[P], below: &[P], tol_z: f64) -> bool {
387 let (Some(a0), Some(a1), Some(b0), Some(b1)) = (above.first(), above.last(), below.first(), below.last())
388 else {
389 return false;
390 };
391 let (from, to) = (a0[0].max(b0[0]), a1[0].min(b1[0]));
392 if from > to {
393 return false;
394 }
395 let mid = 0.5 * (from + to);
396 match (z_at(above, mid), z_at(below, mid)) {
397 (Some(za), Some(zb)) => (za - zb).abs() <= tol_z,
398 _ => false,
399 }
400}
401
402fn z_at(chain: &[P], s: f64) -> Option<f64> {
403 let first = chain.first()?;
404 if chain.len() == 1 {
405 return Some(first[1]);
406 }
407 for pair in chain.windows(2) {
408 if s >= pair[0][0] && s <= pair[1][0] {
409 let width = pair[1][0] - pair[0][0];
410 let t = if width <= f64::EPSILON {
411 0.0
412 } else {
413 (s - pair[0][0]) / width
414 };
415 return Some(pair[0][1] + (pair[1][1] - pair[0][1]) * t);
416 }
417 }
418 None
419}
420
421fn chain_of(cuts: &[Cut], tol: f64) -> Vec<P> {
423 let mut points: Vec<P> = cuts.iter().flat_map(|c| c.profile).collect();
424 points.sort_by(|a, b| a[0].total_cmp(&b[0]).then(a[1].total_cmp(&b[1])));
425 points.dedup_by(|b, a| (a[0] - b[0]).abs() <= tol * 1e-3 && (a[1] - b[1]).abs() <= tol * 1e-3);
426 points
427}
428
429fn join<'a>(chains: impl Iterator<Item = &'a Vec<P>>, tol_s: f64, tol_z: f64) -> (Vec<Vec<P>>, Vec<Vec<P>>) {
433 let mut spans: Vec<Vec<P>> = Vec::new();
434 let mut steps: Vec<Vec<P>> = Vec::new();
435 for chain in chains {
436 let Some(first) = chain.first() else { continue };
437 match spans.last_mut() {
438 Some(span) => {
439 let last = *span.last().expect("a span has a point");
440 if (first[0] - last[0]).abs() <= tol_s {
441 if (first[1] - last[1]).abs() <= tol_z {
442 span.extend(chain.iter().skip(1));
443 } else {
444 steps.push(vec![last, *first]);
445 spans.push(chain.clone());
446 }
447 } else {
448 spans.push(chain.clone());
449 }
450 }
451 None => spans.push(chain.clone()),
452 }
453 }
454 (spans, steps)
455}
456
457fn crosses_box(trace: &[P], bbox: [f64; 4]) -> bool {
460 let pad = 1e-9 * (1.0 + bbox[2].abs().max(bbox[3].abs()));
461 let (lo, hi) = ([bbox[0] - pad, bbox[1] - pad], [bbox[2] + pad, bbox[3] + pad]);
462 trace.windows(2).any(|pair| {
463 let (p, q) = (pair[0], pair[1]);
464 let (mut t0, mut t1) = (0.0f64, 1.0f64);
465 for axis in 0..2 {
466 let d = q[axis] - p[axis];
467 if d.abs() < 1e-15 {
468 if p[axis] < lo[axis] || p[axis] > hi[axis] {
469 return false;
470 }
471 } else {
472 let (a, b) = ((lo[axis] - p[axis]) / d, (hi[axis] - p[axis]) / d);
473 t0 = t0.max(a.min(b));
474 t1 = t1.min(a.max(b));
475 if t0 > t1 {
476 return false;
477 }
478 }
479 }
480 true
481 })
482}