use super::{DexelGrid, StockSimError};
use crate::model::{Point3, RigidTransform};
#[derive(Clone, Debug, PartialEq)]
pub struct StockEnvelope {
origin_x: f64,
origin_y: f64,
cell_x: f64,
cell_y: f64,
nx: u32,
ny: u32,
tops: Vec<Option<f64>>,
max_top: Option<f64>,
to_stock: RigidTransform,
}
impl StockEnvelope {
pub fn new(grid: &DexelGrid, to_stock: RigidTransform) -> Result<Self, StockSimError> {
to_stock.validate().map_err(|e| StockSimError::InvalidInput(format!("stock envelope frame: {e}")))?;
if to_stock.apply_vector(Point3::Z).distance(Point3::Z) > 1e-9 {
return Err(StockSimError::InvalidInput(
"stock envelope frame must keep +Z vertical (a rotation about Z and a translation)".into(),
));
}
let tops: Vec<Option<f64>> = grid.columns.iter().map(|c| c.top()).collect();
let max_top = tops.iter().flatten().copied().fold(None, |m: Option<f64>, t| Some(m.map_or(t, |m| m.max(t))));
Ok(Self {
origin_x: grid.origin.x,
origin_y: grid.origin.y,
cell_x: grid.cell_x,
cell_y: grid.cell_y,
nx: grid.nx,
ny: grid.ny,
tops,
max_top,
to_stock,
})
}
fn to_link_z(&self, z: f64) -> f64 {
z - self.to_stock.translation.z
}
pub fn max_top(&self) -> Option<f64> {
self.max_top.map(|z| self.to_link_z(z))
}
pub fn filtered(&self, mut keep: impl FnMut(f64, [Point3; 4]) -> bool) -> Self {
let to_link = self.to_stock.inverse();
let mut out = self.clone();
for iy in 0..self.ny {
for ix in 0..self.nx {
let k = (iy * self.nx + ix) as usize;
let Some(top) = self.tops[k] else { continue };
let (x0, y0) = (self.origin_x + ix as f64 * self.cell_x, self.origin_y + iy as f64 * self.cell_y);
let corner = |x: f64, y: f64| {
let p = to_link.apply_point(Point3::new(x, y, 0.0));
Point3::new(p.x, p.y, 0.0)
};
let corners = [corner(x0, y0), corner(x0 + self.cell_x, y0), corner(x0 + self.cell_x, y0 + self.cell_y), corner(x0, y0 + self.cell_y)];
if !keep(self.to_link_z(top), corners) {
out.tops[k] = None;
}
}
}
out.max_top = out.tops.iter().flatten().copied().fold(None, |m: Option<f64>, t| Some(m.map_or(t, |m| m.max(t))));
out
}
pub fn cell(&self) -> (f64, f64) {
(self.cell_x, self.cell_y)
}
pub fn top_at(&self, p: Point3, radius: f64) -> Option<f64> {
self.top_over_segment(p, p, radius)
}
pub fn top_over_segment(&self, a: Point3, b: Point3, radius: f64) -> Option<f64> {
if !a.is_finite() || !b.is_finite() || radius.is_nan() || radius == f64::INFINITY {
return self.max_top();
}
let r = radius.max(0.0);
let sa = self.to_stock.apply_point(a);
let sb = self.to_stock.apply_point(b);
let (p, q) = ((sa.x, sa.y), (sb.x, sb.y));
let fx = |x: f64| ((x - self.origin_x) / self.cell_x).floor();
let fy = |y: f64| ((y - self.origin_y) / self.cell_y).floor();
let ix0 = (fx(p.0.min(q.0) - r) - 1.0).max(0.0);
let ix1 = (fx(p.0.max(q.0) + r) + 1.0).min(self.nx as f64 - 1.0);
let iy0 = (fy(p.1.min(q.1) - r) - 1.0).max(0.0);
let iy1 = (fy(p.1.max(q.1) + r) + 1.0).min(self.ny as f64 - 1.0);
if ix0 > ix1 || iy0 > iy1 {
return None;
}
let mut best: Option<f64> = None;
for iy in iy0 as u32..=iy1 as u32 {
for ix in ix0 as u32..=ix1 as u32 {
let Some(top) = self.tops[(iy * self.nx + ix) as usize] else { continue };
if best.is_some_and(|b| b >= top) {
continue;
}
let x0 = self.origin_x + ix as f64 * self.cell_x;
let y0 = self.origin_y + iy as f64 * self.cell_y;
if segment_rect_distance(p, q, (x0, y0), (x0 + self.cell_x, y0 + self.cell_y)) <= r {
best = Some(top);
}
}
}
best.map(|z| self.to_link_z(z))
}
}
use crate::geom::point_segment_distance_2d as point_segment_distance;
fn point_rect_distance(p: (f64, f64), lo: (f64, f64), hi: (f64, f64)) -> f64 {
let dx = (lo.0 - p.0).max(0.0).max(p.0 - hi.0);
let dy = (lo.1 - p.1).max(0.0).max(p.1 - hi.1);
(dx * dx + dy * dy).sqrt()
}
fn segment_meets_rect(a: (f64, f64), b: (f64, f64), lo: (f64, f64), hi: (f64, f64)) -> bool {
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let (mut t0, mut t1) = (0.0f64, 1.0f64);
for (pk, qk) in [(-dx, a.0 - lo.0), (dx, hi.0 - a.0), (-dy, a.1 - lo.1), (dy, hi.1 - a.1)] {
if pk == 0.0 {
if qk < 0.0 {
return false;
}
} else {
let t = qk / pk;
if pk < 0.0 {
t0 = t0.max(t);
} else {
t1 = t1.min(t);
}
if t0 > t1 {
return false;
}
}
}
true
}
fn segment_rect_distance(a: (f64, f64), b: (f64, f64), lo: (f64, f64), hi: (f64, f64)) -> f64 {
if segment_meets_rect(a, b, lo, hi) {
return 0.0;
}
let ends = point_rect_distance(a, lo, hi).min(point_rect_distance(b, lo, hi));
[(lo.0, lo.1), (hi.0, lo.1), (hi.0, hi.1), (lo.0, hi.1)]
.into_iter()
.map(|c| point_segment_distance(c, a, b))
.fold(ends, f64::min)
}