use super::{
transfer_budget::TransferBudget,
transfer_math::*,
transfer_occlusion::Occluder,
transfer_points_index::PointGrid,
transfer_surface::Observation,
transfer_types::*,
};
use nalgebra::{Matrix3, SymmetricEigen};
const MAX_POINTS: usize = 2_000_000;
const MAX_VIEWPOINTS: usize = 4_096;
pub(super) struct PointSurface {
positions: Vec<Point>,
colors: Vec<u8>,
normals: Vec<Point>,
stations: Vec<u32>,
viewpoints: Vec<Point>,
grid: PointGrid,
orientation: PointOrientation,
radius: f64,
min_neighbors: usize,
max_neighbors: usize,
band: f64,
distance: f64,
normal_dot: f64,
behind: f64,
support: Vec<(u32, f64)>,
scratch: Vec<(u32, f64)>,
}
impl PointSurface {
pub fn new(
request: &MeshTransferRequest,
spec: &TransferSourcePoints,
payload: &TransferPointPayload<'_>,
frame: &TransferFrame,
budget: &mut TransferBudget,
) -> Result<Self, String> {
let count = spec.point_count as usize;
if count == 0 || count > MAX_POINTS || payload.positions.len() != count * 3 || payload.colors.len() != count * 3 {
return Err("Transfer point payload must carry 1..2,000,000 points with matching RGB8 colours".into());
}
if !spec.neighborhood_radius_metres.is_finite() || spec.neighborhood_radius_metres <= 0. || spec.neighborhood_radius_metres > 0.5
|| spec.min_neighbors < 3 || spec.max_neighbors < spec.min_neighbors || spec.max_neighbors > 256
|| !spec.surface_band_metres.is_finite() || spec.surface_band_metres <= 0. || spec.surface_band_metres > spec.neighborhood_radius_metres
{
return Err("Transfer point fit needs a radius within 0.5 m, 3..256 neighbours and a surface band within the radius".into());
}
let has_normals = !payload.normals.is_empty();
let has_stations = !payload.stations.is_empty() || !spec.viewpoints.is_empty();
match spec.orientation {
PointOrientation::SourceNormals if !has_normals || has_stations => return Err("Transfer source-normals orientation needs oriented per-point normals and no stations".into()),
PointOrientation::Viewpoints if has_normals || payload.stations.len() != count || spec.viewpoints.is_empty() || spec.viewpoints.len() > MAX_VIEWPOINTS => return Err("Transfer viewpoint orientation needs one station index per point, 1..4096 stations and no normals".into()),
PointOrientation::TargetReferenced if has_normals || has_stations => return Err("Transfer target-referenced orientation must not silently ignore supplied normals or stations".into()),
_ => {}
}
if has_normals && payload.normals.len() != count * 3 {
return Err("Transfer point normals must be one unit vector per point".into());
}
if payload.positions.iter().any(|v| !v.is_finite() || v.abs() > 1e12)
|| spec.viewpoints.iter().flatten().any(|v| !v.is_finite() || v.abs() > 1e12)
|| payload.stations.iter().any(|s| *s as usize >= spec.viewpoints.len())
{
return Err("Transfer point coordinates, stations or viewpoints exceed their finite bounds".into());
}
budget.reserve(count * 27 + payload.normals.len() * 8 + payload.stations.len() * 4)?;
budget.charge(count)?;
let positions: Vec<Point> = payload.positions.chunks_exact(3).map(|p| transform(frame, [p[0], p[1], p[2]])).collect();
let rotate = |n: Point| -> Point { std::array::from_fn(|i| (0..3).map(|j| frame.rotation[i][j] * n[j]).sum()) };
let mut normals = Vec::with_capacity(payload.normals.len() / 3);
for n in payload.normals.chunks_exact(3) {
let n = [f64::from(n[0]), f64::from(n[1]), f64::from(n[2])];
let length = dot(n, n).sqrt();
if !length.is_finite() || length < 0.5 || length > 2. {
return Err("Transfer point normals must be finite unit vectors".into());
}
normals.push(rotate(n.map(|v| v / length)));
}
if request.max_distance_metres > 16. * spec.neighborhood_radius_metres {
return Err("Transfer distance bound must not exceed 16 times the point support radius".into());
}
let grid = PointGrid::build(&positions, spec.neighborhood_radius_metres, budget)?;
Ok(Self {
positions,
colors: payload.colors.to_vec(),
normals,
stations: payload.stations.to_vec(),
viewpoints: spec.viewpoints.iter().map(|p| transform(frame, *p)).collect(),
grid,
orientation: spec.orientation,
radius: spec.neighborhood_radius_metres,
min_neighbors: spec.min_neighbors as usize,
max_neighbors: spec.max_neighbors as usize,
band: spec.surface_band_metres,
distance: request.max_distance_metres,
normal_dot: request.min_normal_dot,
behind: request.max_behind_metres,
support: Vec::new(),
scratch: Vec::new(),
})
}
pub fn observe(
&mut self,
point: Point,
target_normal: Point,
occluder: &mut Occluder,
budget: &mut TransferBudget,
) -> Result<(Observation, [f64; 4]), String> {
budget.charge(1)?;
let unknown = |o| Ok((o, [0.; 4]));
let rounding = |p: Point| 16. * f64::EPSILON * point.iter().chain(&p).fold(1_f64, |m, v| m.max(v.abs()));
let referenced = self.orientation == PointOrientation::TargetReferenced;
self.support.clear();
self.grid.within(&self.positions, point, self.radius, &mut self.support, budget)?;
let positions = &self.positions;
let depth_of = |index: u32| dot(sub(positions[index as usize], point), target_normal);
let nearest_of = |candidates: &[(u32, f64)], keep: &dyn Fn(u32) -> bool| {
candidates.iter().copied().filter(|&(i, _)| keep(i)).min_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0))).map(|(i, _)| i)
};
let mut nearest = match nearest_of(&self.support, &|_| true) {
Some(index) => index,
None => match self.grid.nearest(positions, point, self.distance, &mut self.scratch, budget)? {
Some((index, _)) => index,
None => return unknown(Observation::Distance),
},
};
let mut anchor = positions[nearest as usize];
let facing_away = match self.orientation {
PointOrientation::SourceNormals => dot(self.normals[nearest as usize], target_normal) < self.normal_dot,
PointOrientation::Viewpoints => dot(sub(self.viewpoints[self.stations[nearest as usize] as usize], anchor), target_normal) < 0.,
PointOrientation::TargetReferenced => false,
};
if facing_away {
return unknown(Observation::Normal);
}
if occluder.blocked(point, anchor, budget)? {
return unknown(Observation::Behind);
}
let thickness = occluder.thickness_behind(point, target_normal, self.distance + self.radius, budget)?;
let behind = match thickness {
Some(t) if referenced => self.behind.min(t / 2.),
_ => self.behind,
};
if depth_of(nearest) < -(behind + rounding(anchor)) {
return unknown(Observation::Behind);
}
let coplanar = |index: u32| depth_of(index) >= -(self.band.min(behind) + rounding(positions[index as usize]));
if referenced && !coplanar(nearest) {
self.scratch.clear();
self.grid.within(positions, point, self.distance, &mut self.scratch, budget)?;
budget.charge(self.scratch.len())?;
if let Some(front) = nearest_of(&self.scratch, &coplanar) {
if !occluder.blocked(point, positions[front as usize], budget)? {
nearest = front;
anchor = positions[front as usize];
}
}
}
let support_floor = match (self.orientation, thickness) {
(PointOrientation::TargetReferenced, _) if coplanar(nearest) => self.band.min(behind),
(PointOrientation::TargetReferenced, _) => behind,
(_, Some(t)) => t,
(_, None) => f64::INFINITY,
};
let offset2: f64 = (0..3).map(|a| (anchor[a] - point[a]).powi(2)).sum();
if offset2 > self.radius * self.radius / 4. || !self.support.iter().any(|&(index, _)| index == nearest) {
self.support.clear();
self.grid.within(positions, anchor, self.radius, &mut self.support, budget)?;
}
if self.support.len() > self.max_neighbors {
budget.charge(self.support.len())?;
self.support.sort_unstable_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)));
self.support.truncate(self.max_neighbors);
}
budget.charge(self.support.len())?;
let (normals, orientation) = (&self.normals, self.orientation);
self.support.retain(|&(index, _)| {
let depth = dot(sub(positions[index as usize], point), target_normal);
if depth < -support_floor {
return false;
}
orientation != PointOrientation::SourceNormals || dot(normals[index as usize], normals[nearest as usize]) > 0.
});
let Some(mut plane) = self.fit(budget)? else { return unknown(Observation::Sparse) };
if self.orientation == PointOrientation::Viewpoints {
let station = self.viewpoints[self.stations[nearest as usize] as usize];
let side = dot(sub(station, plane.centroid), plane.normal).signum();
let (viewpoints, stations) = (&self.viewpoints, &self.stations);
self.support.retain(|(index, _)| {
let s = viewpoints[stations[*index as usize] as usize];
dot(sub(s, plane.centroid), plane.normal).signum() == side || *index == nearest
});
let Some(refit) = self.fit(budget)? else { return unknown(Observation::Sparse) };
plane = refit;
}
if plane.rms > self.band {
return unknown(Observation::Ambiguous);
}
let toward = match self.orientation {
PointOrientation::SourceNormals => {
let mut mean = [0.; 3];
for &(index, _) in &self.support {
let n = self.normals[index as usize];
for a in 0..3 { mean[a] += n[a]; }
}
if dot(mean, mean).sqrt() < 0.5 * self.support.len() as f64 {
return unknown(Observation::Ambiguous);
}
mean
}
PointOrientation::Viewpoints => sub(self.viewpoints[self.stations[nearest as usize] as usize], plane.centroid),
PointOrientation::TargetReferenced => target_normal,
};
let normal = if dot(plane.normal, toward) < 0. { plane.normal.map(|v| -v) } else { plane.normal };
if dot(normal, target_normal) < self.normal_dot {
return unknown(Observation::Normal);
}
let offset = dot(sub(point, plane.centroid), normal);
let closest: Point = std::array::from_fn(|a| point[a] - normal[a] * offset);
if offset.abs() > self.distance {
return unknown(Observation::Distance);
}
let depth = dot(sub(closest, point), target_normal);
if depth < -(behind + rounding(closest)) {
return unknown(Observation::Behind);
}
let mut color = [0.; 3];
for &(index, _) in &self.support {
let rgb = &self.colors[index as usize * 3..index as usize * 3 + 3];
for (sum, byte) in color.iter_mut().zip(rgb) {
*sum += f64::from(*byte) / 255.;
}
}
let n = self.support.len() as f64;
Ok((Observation::Observed, [color[0] / n, color[1] / n, color[2] / n, 1.]))
}
fn fit(&self, budget: &mut TransferBudget) -> Result<Option<Plane>, String> {
budget.charge(self.support.len() + 1)?;
if self.support.len() < self.min_neighbors {
return Ok(None);
}
let n = self.support.len() as f64;
let mut centroid = [0.; 3];
for &(index, _) in &self.support {
let p = self.positions[index as usize];
for a in 0..3 { centroid[a] += p[a] / n; }
}
let mut covariance = Matrix3::zeros();
for &(index, _) in &self.support {
let d = sub(self.positions[index as usize], centroid);
for i in 0..3 { for j in 0..3 { covariance[(i, j)] += d[i] * d[j] / n; } }
}
let eigen = SymmetricEigen::new(covariance);
let values: [f64; 3] = std::array::from_fn(|i| eigen.eigenvalues[i]);
let mut order = [0_usize, 1, 2];
order.sort_by(|a, b| values[*a].total_cmp(&values[*b]));
let smallest = order[0];
let column = eigen.eigenvectors.column(smallest);
let normal = [column[0], column[1], column[2]];
let length = dot(normal, normal).sqrt();
if !length.is_finite() || length == 0. || values[order[1]] <= 1e-12 * self.radius * self.radius {
return Ok(None);
}
Ok(Some(Plane { centroid, normal: normal.map(|v| v / length), rms: values[smallest].max(0.).sqrt() }))
}
}
struct Plane {
centroid: Point,
normal: Point,
rms: f64,
}
#[cfg(test)]
#[path = "transfer_points_tests.rs"]
mod tests;