use std::collections::{BTreeMap, HashMap};
use energyml_geometry::geometry::{Patch, Topology};
use energyml_geometry::section::{self, Cut};
use crate::model::{CutShape, SectionTrace, SurfaceCut};
type P = [f64; 2];
#[derive(Clone, Copy, Debug)]
struct Cell {
corners: [u32; 8],
column: (u32, u32),
layer: u32,
bbox: [f64; 4],
mean_z: f64,
}
#[derive(Clone, Debug)]
pub struct Grid {
points: Vec<[f64; 3]>,
cells: Vec<Cell>,
}
impl Grid {
pub fn from_patch(patch: &Patch) -> Result<Grid, String> {
let Topology::Hexahedra {
cells, dimensions, ..
} = &patch.topology
else {
return Err("the grid did not read as hexahedra".to_string());
};
let [ni, nj, nk] = *dimensions;
let per_layer = ni * nj;
if per_layer == 0 {
return Err(format!("the grid is {ni}x{nj}x{nk}"));
}
let mut out = Vec::with_capacity(cells.len());
for (position, corners) in cells.iter().enumerate() {
let Some(number) = patch.topology.cell_at(position) else {
continue;
};
let number = number as usize;
let (layer, column) = (number / per_layer, number % per_layer);
let at: Vec<[f64; 3]> = corners.iter().map(|c| patch.points[*c as usize]).collect();
if at.iter().any(|p| p.iter().any(|v| !v.is_finite())) {
continue;
}
let mut bbox = [f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY];
for p in &at {
bbox = [
bbox[0].min(p[0]),
bbox[1].min(p[1]),
bbox[2].max(p[0]),
bbox[3].max(p[1]),
];
}
out.push(Cell {
corners: *corners,
column: ((column % ni) as u32, (column / ni) as u32),
layer: layer as u32,
bbox,
mean_z: at.iter().map(|p| p[2]).sum::<f64>() / 8.0,
});
}
if out.is_empty() {
return Err("the grid has no defined cell".to_string());
}
Ok(Grid {
points: patch.points.clone(),
cells: out,
})
}
pub fn cut(&self, sections: &[SectionTrace]) -> Vec<SurfaceCut> {
sections.iter().filter_map(|one| self.cut_along(one)).collect()
}
fn cut_along(&self, section: &SectionTrace) -> Option<SurfaceCut> {
let trace = §ion.trace;
let length: f64 = trace
.windows(2)
.map(|p| (p[1][0] - p[0][0]).hypot(p[1][1] - p[0][1]))
.sum();
let tol_s = 1e-6 * length.max(1.0);
let mut cut_cells: Vec<CellCut> = Vec::new();
let mut clipped = 0usize;
for cell in &self.cells {
if !crosses_box(trace, cell.bbox) {
continue;
}
match self.cut_cell(cell, trace, tol_s) {
Tried::Cut(done) => cut_cells.push(done),
Tried::Clipped => clipped += 1,
Tried::Missed => {}
}
}
if cut_cells.is_empty() {
return None;
}
let z_max = cut_cells
.iter()
.flat_map(|c| c.top.iter().chain(&c.bottom))
.fold(1.0f64, |m, p| m.max(p[1].abs()));
let tol_z = 1e-6 * z_max;
let mut columns: BTreeMap<(u32, u32), Vec<CellCut>> = BTreeMap::new();
for done in cut_cells {
columns.entry(done.column).or_default().push(done);
}
let mut pieces: Vec<Piece> = Vec::new();
for (_, mut cells) in columns {
cells.sort_by(|a, b| a.s_range().0.total_cmp(&b.s_range().0));
let mut stretches: Vec<Vec<CellCut>> = Vec::new();
let mut reach = f64::NEG_INFINITY;
for done in cells {
let (from, to) = done.s_range();
if stretches.is_empty() || from > reach + tol_s {
stretches.push(Vec::new());
reach = to;
} else {
reach = reach.max(to);
}
stretches
.last_mut()
.expect("a stretch was just pushed")
.push(done);
}
for stretch in stretches {
pieces.push(Piece::of(stretch, tol_z));
}
}
pieces.sort_by(|a, b| a.start().total_cmp(&b.start()));
let (top, mut faults) = join(pieces.iter().map(|p| &p.top), tol_s, tol_z);
let (bottom, floor_steps) = join(pieces.iter().map(|p| &p.bottom), tol_s, tol_z);
faults.extend(floor_steps);
let mut fills: Vec<Vec<P>> = Vec::new();
let mut open: HashMap<(u32, u32), usize> = HashMap::new();
let mut runs: Vec<Run> = Vec::new();
for piece in &pieces {
for run in &piece.runs {
let joined = open
.get(&run.key)
.copied()
.filter(|at| runs[*at].meets(run, tol_s, tol_z));
match joined {
Some(at) => runs[at].extend(run),
None => {
open.insert(run.key, runs.len());
runs.push(run.clone());
}
}
}
}
for run in runs {
let mut ring = run.upper;
ring.extend(run.lower.into_iter().rev());
if ring.len() >= 3 {
fills.push(ring);
}
}
let worth =
|spans: Vec<Vec<P>>| -> Vec<Vec<P>> { spans.into_iter().filter(|s| s.len() >= 2).collect() };
let mut notes = Vec::new();
if clipped > 0 {
notes.push(format!(
"{clipped} cell(s) of the grid are met by this section at one face only (a corner of an inclined \
cell) and are left out of its envelope"
));
}
Some(SurfaceCut {
section: section.key,
shape: CutShape::Envelope {
top: worth(top),
bottom: worth(bottom),
fills,
faults,
},
notes,
sources: Vec::new(),
})
}
fn cut_cell(&self, cell: &Cell, trace: &[P], tol: f64) -> Tried {
let at = |n: usize| self.points[cell.corners[n] as usize];
let ring_z = |from: usize| (from..from + 4).map(|n| at(n)[2]).sum::<f64>() / 4.0;
let (roof, floor) = if ring_z(0) >= ring_z(4) { (0, 4) } else { (4, 0) };
let chain = |from: usize| -> Vec<P> {
let local: Vec<[f64; 3]> = (from..from + 4).map(at).collect();
let found = section::cut(&local, &[[0, 1, 2], [0, 2, 3]], trace);
chain_of(&found, tol)
};
let (top, bottom) = (chain(roof), chain(floor));
match (top.is_empty(), bottom.is_empty()) {
(true, true) => Tried::Missed,
(false, false) => Tried::Cut(CellCut {
column: cell.column,
layer: cell.layer,
mean_z: cell.mean_z,
top,
bottom,
}),
_ => Tried::Clipped,
}
}
}
enum Tried {
Cut(CellCut),
Clipped,
Missed,
}
#[derive(Clone, Debug)]
struct CellCut {
column: (u32, u32),
layer: u32,
mean_z: f64,
top: Vec<P>,
bottom: Vec<P>,
}
impl CellCut {
fn s_range(&self) -> (f64, f64) {
let ends = [
self.top.first(),
self.top.last(),
self.bottom.first(),
self.bottom.last(),
];
let s = ends.iter().flatten().map(|p| p[0]);
(
s.clone().fold(f64::INFINITY, f64::min),
s.fold(f64::NEG_INFINITY, f64::max),
)
}
}
#[derive(Clone, Debug)]
struct Piece {
top: Vec<P>,
bottom: Vec<P>,
runs: Vec<Run>,
}
impl Piece {
fn start(&self) -> f64 {
self.top[0][0].min(self.bottom[0][0])
}
fn of(mut cells: Vec<CellCut>, tol_z: f64) -> Piece {
cells.sort_by(|a, b| b.mean_z.total_cmp(&a.mean_z));
let mut runs: Vec<Run> = Vec::new();
let mut last: Option<&CellCut> = None;
for cell in &cells {
let continues = last.is_some_and(|prev| {
prev.layer.abs_diff(cell.layer) == 1 && touches(&prev.bottom, &cell.top, tol_z)
});
if continues {
let run = runs.last_mut().expect("a run is open");
run.lower = cell.bottom.clone();
run.key = (run.key.0.min(cell.layer), run.key.1.max(cell.layer));
} else {
runs.push(Run {
key: (cell.layer, cell.layer),
upper: cell.top.clone(),
lower: cell.bottom.clone(),
});
}
last = Some(cell);
}
let highest = cells.first().expect("a stretch has a cell");
let lowest = cells.last().expect("a stretch has a cell");
Piece {
top: highest.top.clone(),
bottom: lowest.bottom.clone(),
runs,
}
}
}
#[derive(Clone, Debug)]
struct Run {
key: (u32, u32),
upper: Vec<P>,
lower: Vec<P>,
}
impl Run {
fn meets(&self, next: &Run, tol_s: f64, tol_z: f64) -> bool {
let same = |a: Option<&P>, b: Option<&P>| match (a, b) {
(Some(a), Some(b)) => (a[0] - b[0]).abs() <= tol_s && (a[1] - b[1]).abs() <= tol_z,
_ => false,
};
same(self.upper.last(), next.upper.first()) && same(self.lower.last(), next.lower.first())
}
fn extend(&mut self, next: &Run) {
self.upper.extend(next.upper.iter().skip(1));
self.lower.extend(next.lower.iter().skip(1));
}
}
fn touches(above: &[P], below: &[P], tol_z: f64) -> bool {
let (Some(a0), Some(a1), Some(b0), Some(b1)) = (above.first(), above.last(), below.first(), below.last())
else {
return false;
};
let (from, to) = (a0[0].max(b0[0]), a1[0].min(b1[0]));
if from > to {
return false;
}
let mid = 0.5 * (from + to);
match (z_at(above, mid), z_at(below, mid)) {
(Some(za), Some(zb)) => (za - zb).abs() <= tol_z,
_ => false,
}
}
fn z_at(chain: &[P], s: f64) -> Option<f64> {
let first = chain.first()?;
if chain.len() == 1 {
return Some(first[1]);
}
for pair in chain.windows(2) {
if s >= pair[0][0] && s <= pair[1][0] {
let width = pair[1][0] - pair[0][0];
let t = if width <= f64::EPSILON {
0.0
} else {
(s - pair[0][0]) / width
};
return Some(pair[0][1] + (pair[1][1] - pair[0][1]) * t);
}
}
None
}
fn chain_of(cuts: &[Cut], tol: f64) -> Vec<P> {
let mut points: Vec<P> = cuts.iter().flat_map(|c| c.profile).collect();
points.sort_by(|a, b| a[0].total_cmp(&b[0]).then(a[1].total_cmp(&b[1])));
points.dedup_by(|b, a| (a[0] - b[0]).abs() <= tol * 1e-3 && (a[1] - b[1]).abs() <= tol * 1e-3);
points
}
fn join<'a>(chains: impl Iterator<Item = &'a Vec<P>>, tol_s: f64, tol_z: f64) -> (Vec<Vec<P>>, Vec<Vec<P>>) {
let mut spans: Vec<Vec<P>> = Vec::new();
let mut steps: Vec<Vec<P>> = Vec::new();
for chain in chains {
let Some(first) = chain.first() else { continue };
match spans.last_mut() {
Some(span) => {
let last = *span.last().expect("a span has a point");
if (first[0] - last[0]).abs() <= tol_s {
if (first[1] - last[1]).abs() <= tol_z {
span.extend(chain.iter().skip(1));
} else {
steps.push(vec![last, *first]);
spans.push(chain.clone());
}
} else {
spans.push(chain.clone());
}
}
None => spans.push(chain.clone()),
}
}
(spans, steps)
}
fn crosses_box(trace: &[P], bbox: [f64; 4]) -> bool {
let pad = 1e-9 * (1.0 + bbox[2].abs().max(bbox[3].abs()));
let (lo, hi) = ([bbox[0] - pad, bbox[1] - pad], [bbox[2] + pad, bbox[3] + pad]);
trace.windows(2).any(|pair| {
let (p, q) = (pair[0], pair[1]);
let (mut t0, mut t1) = (0.0f64, 1.0f64);
for axis in 0..2 {
let d = q[axis] - p[axis];
if d.abs() < 1e-15 {
if p[axis] < lo[axis] || p[axis] > hi[axis] {
return false;
}
} else {
let (a, b) = ((lo[axis] - p[axis]) / d, (hi[axis] - p[axis]) / d);
t0 = t0.max(a.min(b));
t1 = t1.min(a.max(b));
if t0 > t1 {
return false;
}
}
}
true
})
}