use super::super::page_raster::Raster;
use super::super::transfer::acceptance_tests::{control, mesh, GREEN, THICKNESS, THIN_WALL_IFC, WALL};
use super::super::transfer::tests::{color, identity};
use super::super::transfer::{plan_mesh_transfer, plan_point_transfer};
use super::super::transfer_target::TargetTriangle;
use super::*;
use crate::appearance::tests::apply;
use crate::appearance::{register_scan_correspondences, AppearanceRaster, MeshTransferPlan};
const RED: [u8; 3] = [255, 0, 0];
const BLUE: [u8; 3] = [0, 0, 255];
fn jitter(i: usize, salt: u32) -> f64 {
let mut h = (i as u32).wrapping_mul(2_654_435_761).wrapping_add(salt.wrapping_mul(40_503));
h ^= h >> 15;
h = h.wrapping_mul(2_246_822_519);
h ^= h >> 13;
f64::from(h % 20_001) / 10_000. - 1.
}
#[derive(Default, Clone)]
struct Cloud {
positions: Vec<f64>,
colors: Vec<u8>,
normals: Vec<f32>,
stations: Vec<u32>,
}
struct Sheet {
y: f64,
x: [f64; 2],
spacing: f64,
noise: f64,
color: [u8; 3],
}
impl Cloud {
fn sheet(&mut self, sheet: Sheet, facing_negative_y: bool) {
let Sheet { y, x, spacing, noise, color } = sheet;
let station = u32::from(!facing_negative_y);
let (nx, nz) = (((x[1] - x[0]) / spacing).round() as usize, (1. / spacing).round() as usize);
let sign = if facing_negative_y { -1. } else { 1. };
for i in 0..=nx {
for k in 0..=nz {
let index = self.positions.len() / 3;
let px = x[0] + i as f64 * spacing + jitter(index, 1) * spacing * 0.2;
let pz = k as f64 * spacing + jitter(index, 2) * spacing * 0.2;
self.positions.extend([px.clamp(x[0], x[1]), y + jitter(index, 3) * noise, pz.clamp(0., 1.)]);
self.colors.extend(color);
self.normals.extend([0., sign as f32, 0.]);
self.stations.push(station);
}
}
}
fn payload(&self, orientation: PointOrientation) -> TransferPointPayload<'_> {
TransferPointPayload {
positions: &self.positions,
colors: &self.colors,
normals: if orientation == PointOrientation::SourceNormals { &self.normals } else { &[] },
stations: if orientation == PointOrientation::Viewpoints { &self.stations } else { &[] },
}
}
}
fn spec(cloud: &Cloud, orientation: PointOrientation) -> TransferSourcePoints {
TransferSourcePoints {
point_count: (cloud.positions.len() / 3) as u32,
orientation,
neighborhood_radius_metres: 0.03,
min_neighbors: 4,
max_neighbors: 32,
surface_band_metres: 0.003,
viewpoints: if orientation == PointOrientation::Viewpoints { vec![[0.5, -2., 0.5], [0.5, 2., 0.5]] } else { vec![] },
}
}
fn point_request(cloud: &Cloud, orientation: PointOrientation, max_behind: f64) -> (MeshTransferRequest, Vec<u8>) {
let (mut request, _) = control(mesh(), 0.02, max_behind);
request.source = TransferSource::Points(spec(cloud, orientation));
request.source_image = None;
(request, Vec::new())
}
struct Baked {
plan: MeshTransferPlan,
wall: crate::types::mesh::MeshData,
width: u32,
height: u32,
pixels: Vec<u8>,
}
impl Baked {
fn new(cloud: &Cloud, orientation: PointOrientation, max_behind: f64) -> Self {
let (request, rgba) = point_request(cloud, orientation, max_behind);
let plan = plan_point_transfer(THIN_WALL_IFC.as_bytes(), &request, &rgba, &cloud.payload(orientation)).unwrap();
assert_eq!(plan.transfer.source.kind, "points");
assert_eq!(plan.transfer.source.orientation, Some(orientation));
let (wall, width, height, pixels) = {
let output = plan.output.as_ref().expect("applicable point transfer");
assert!(output.plan.exclusions.is_empty());
let reopened = crate::process_geometry(apply(THIN_WALL_IFC, &output.plan).as_bytes());
let wall = reopened.meshes.iter().find(|m| m.express_id == WALL).unwrap().clone();
let asset = &output.assets[0];
let mut reader = png::Decoder::new(std::io::Cursor::new(&asset.png)).read_info().unwrap();
let mut pixels = vec![0; reader.output_buffer_size().unwrap()];
reader.next_frame(&mut pixels).unwrap();
(wall, asset.width, asset.height, pixels)
};
Self { plan, wall, width, height, pixels }
}
#[track_caller]
fn expect(&self, point: Point, expected: [f64; 4]) {
let actual = color(&self.wall, Raster::new(self.width, self.height, &self.pixels).unwrap(), point, [false, false]);
for (a, e) in actual.iter().zip(expected) {
assert!((a - e).abs() < 0.03, "{point:?}: {actual:?} != {expected:?}");
}
}
}
const ALL: [PointOrientation; 3] = [PointOrientation::SourceNormals, PointOrientation::Viewpoints, PointOrientation::TargetReferenced];
const RED_RGBA: [f64; 4] = [1., 0., 0., 1.];
const BLUE_RGBA: [f64; 4] = [0., 0., 1., 1.];
#[test]
fn issue_4381_point_cloud_thin_wall_faces_observe_only_their_own_side_under_every_orientation() {
let mut cloud = Cloud::default();
cloud.sheet(Sheet { y: -0.001, x: [0., 1.], spacing: 0.005, noise: 0.001, color: RED }, true);
cloud.sheet(Sheet { y: THICKNESS + 0.001, x: [0., 0.5], spacing: 0.005, noise: 0.001, color: BLUE }, false);
for orientation in ALL {
let baked = Baked::new(&cloud, orientation, 0.01);
let coverage = &baked.plan.transfer.coverage;
println!("{orientation:?} coverage {coverage:?}");
assert!(baked.plan.transfer.applicable);
assert!(coverage.observed_raster_interior_texels > 0);
let total = coverage.observed_area_estimate_m2 + coverage.unknown_area_estimate_m2;
assert!((total - 2.).abs() < 1e-9, "{coverage:?}");
assert!(coverage.observed_area_estimate_m2 > 1.4 && coverage.observed_area_estimate_m2 < 1.6, "{orientation:?} {coverage:?}");
assert_eq!(coverage.unknown_distance_samples, 0, "{coverage:?}");
match orientation {
PointOrientation::TargetReferenced => assert!(coverage.unknown_behind_samples > 0 && coverage.unknown_normal_samples == 0, "{coverage:?}"),
_ => assert!(coverage.unknown_normal_samples > 0, "{coverage:?}"),
}
baked.expect([0.3, 0., 0.5], RED_RGBA);
baked.expect([0.8, 0., 0.5], RED_RGBA);
baked.expect([0.3, THICKNESS, 0.5], BLUE_RGBA);
baked.expect([0.8, THICKNESS, 0.5], GREEN);
}
}
#[test]
fn issue_4381_point_capture_inside_the_solid_belongs_to_its_nearest_face_only() {
let mut inside = Cloud::default();
inside.sheet(Sheet { y: 0.001, x: [0., 1.], spacing: 0.005, noise: 0.001, color: RED }, true);
let baked = Baked::new(&inside, PointOrientation::TargetReferenced, 0.01);
let coverage = &baked.plan.transfer.coverage;
println!("inside capture coverage {coverage:?}");
assert!(coverage.observed_area_estimate_m2 > 0.95 && coverage.observed_area_estimate_m2 < 1.05, "{coverage:?}");
assert!(coverage.unknown_behind_samples > 0 && coverage.unknown_normal_samples == 0 && coverage.unknown_distance_samples == 0, "{coverage:?}");
baked.expect([0.3, 0., 0.5], RED_RGBA);
baked.expect([0.3, THICKNESS, 0.5], GREEN);
let mut deep = Cloud::default();
deep.sheet(Sheet { y: THICKNESS - 0.001, x: [0., 1.], spacing: 0.005, noise: 0.001, color: RED }, true);
let baked = Baked::new(&deep, PointOrientation::TargetReferenced, 0.01);
let coverage = &baked.plan.transfer.coverage;
assert!(coverage.observed_area_estimate_m2 > 0.95 && coverage.observed_area_estimate_m2 < 1.05, "{coverage:?}");
assert!(coverage.unknown_behind_samples > 0, "{coverage:?}");
baked.expect([0.3, 0., 0.5], GREEN);
baked.expect([0.3, THICKNESS, 0.5], RED_RGBA);
for orientation in [PointOrientation::SourceNormals, PointOrientation::Viewpoints] {
let baked = Baked::new(&deep, orientation, 0.01);
let coverage = &baked.plan.transfer.coverage;
assert!(coverage.unknown_normal_samples > 4000 && coverage.observed_samples > 4000, "{orientation:?} {coverage:?}");
baked.expect([0.3, 0., 0.5], RED_RGBA);
baked.expect([0.3, THICKNESS, 0.5], GREEN);
}
}
#[test]
fn issue_4381_unoriented_capture_in_front_of_the_face_is_preferred_over_one_inside_the_solid() {
let slab = |y: f64, sign: f64| -> [TargetTriangle; 2] {
let c = [[0., y, 0.], [1., y, 0.], [1., y, 1.], [0., y, 1.]];
[TargetTriangle { points: [c[0], c[1], c[2]], normal: [0., sign, 0.], area: 0.5 }, TargetTriangle { points: [c[0], c[2], c[3]], normal: [0., sign, 0.], area: 0.5 }]
};
let targets: Vec<TargetTriangle> = slab(0., -1.).into_iter().chain(slab(0.1, 1.)).collect();
let observe = |cloud: &Cloud, point: Point, normal: Point| -> (Observation, [f64; 4]) {
let (mut request, _) = point_request(cloud, PointOrientation::TargetReferenced, 0.07);
request.max_distance_metres = 0.07;
let TransferSource::Points(spec) = &mut request.source else { unreachable!() };
spec.neighborhood_radius_metres = 0.04;
spec.surface_band_metres = 0.006;
spec.min_neighbors = 6;
spec.max_neighbors = 48;
let (spec, frame, payload) = (spec.clone(), identity(), cloud.payload(PointOrientation::TargetReferenced));
let mut budget = TransferBudget::new();
let mut surface = PointSurface::new(&request, &spec, &payload, &frame, &mut budget).unwrap();
let mut occluder = Occluder::new(&targets, &mut budget).unwrap();
surface.observe(point, normal, &mut occluder, &mut budget).unwrap()
};
let (front, back) = (([0.5, 0., 0.5], [0., -1., 0.]), ([0.5, 0.1, 0.5], [0., 1., 0.]));
let mut both = Cloud::default();
both.sheet(Sheet { y: -0.055, x: [0., 1.], spacing: 0.01, noise: 0.001, color: BLUE }, true);
both.sheet(Sheet { y: 0.045, x: [0., 1.], spacing: 0.01, noise: 0.001, color: RED }, false);
let (observation, rgba) = observe(&both, front.0, front.1);
assert_eq!(observation, Observation::Observed);
assert!((rgba[2] - 1.).abs() < 0.02 && rgba[0] < 0.02, "front face takes the front capture, not the nearer in-solid one: {rgba:?}");
assert_eq!(observe(&both, back.0, back.1).0, Observation::Behind, "5.5 cm inside from the far face is beyond its 5 cm midplane");
let mut inside = Cloud::default();
inside.sheet(Sheet { y: 0.045, x: [0., 1.], spacing: 0.01, noise: 0.001, color: RED }, false);
let (observation, rgba) = observe(&inside, front.0, front.1);
assert_eq!(observation, Observation::Observed);
assert!((rgba[0] - 1.).abs() < 0.02, "{rgba:?}");
assert_eq!(observe(&inside, back.0, back.1).0, Observation::Behind);
let mut deep = Cloud::default();
deep.sheet(Sheet { y: 0.06, x: [0., 1.], spacing: 0.01, noise: 0.001, color: RED }, false);
assert_eq!(observe(&deep, front.0, front.1).0, Observation::Behind);
assert_eq!(observe(&deep, back.0, back.1).0, Observation::Observed);
let (mut tight, _) = point_request(&inside, PointOrientation::TargetReferenced, 0.02);
tight.max_distance_metres = 0.07;
let TransferSource::Points(spec) = &mut tight.source else { unreachable!() };
spec.neighborhood_radius_metres = 0.04;
spec.surface_band_metres = 0.006;
let (spec, frame, payload) = (spec.clone(), identity(), inside.payload(PointOrientation::TargetReferenced));
let mut budget = TransferBudget::new();
let mut surface = PointSurface::new(&tight, &spec, &payload, &frame, &mut budget).unwrap();
let mut occluder = Occluder::new(&targets, &mut budget).unwrap();
assert_eq!(surface.observe(front.0, front.1, &mut occluder, &mut budget).unwrap().0, Observation::Behind);
}
#[test]
fn issue_4381_point_cloud_holes_and_clutter_stay_unknown_and_colour_is_a_surface_estimate() {
let mut cloud = Cloud::default();
cloud.sheet(Sheet { y: -0.001, x: [0., 0.4], spacing: 0.005, noise: 0.001, color: RED }, true);
cloud.sheet(Sheet { y: -0.001, x: [0.6, 1.], spacing: 0.005, noise: 0.001, color: RED }, true);
let baked = Baked::new(&cloud, PointOrientation::TargetReferenced, 0.01);
let coverage = &baked.plan.transfer.coverage;
assert!(coverage.unknown_distance_samples > 0, "{coverage:?}");
baked.expect([0.2, 0., 0.5], RED_RGBA);
baked.expect([0.5, 0., 0.5], GREEN);
baked.expect([0.5, THICKNESS, 0.5], GREEN);
let mut layered = cloud.clone();
layered.sheet(Sheet { y: -0.009, x: [0., 0.4], spacing: 0.005, noise: 0., color: BLUE }, true);
let (request, rgba) = point_request(&layered, PointOrientation::TargetReferenced, 0.01);
let plan = plan_point_transfer(THIN_WALL_IFC.as_bytes(), &request, &rgba, &layered.payload(PointOrientation::TargetReferenced)).unwrap();
assert!(plan.transfer.coverage.unknown_ambiguous_samples > 0, "{:?}", plan.transfer.coverage);
let mut sparse = Cloud::default();
sparse.sheet(Sheet { y: -0.001, x: [0., 1.], spacing: 0.05, noise: 0., color: RED }, true);
let (request, rgba) = point_request(&sparse, PointOrientation::TargetReferenced, 0.01);
let plan = plan_point_transfer(THIN_WALL_IFC.as_bytes(), &request, &rgba, &sparse.payload(PointOrientation::TargetReferenced)).unwrap();
let coverage = &plan.transfer.coverage;
assert!(coverage.unknown_sparse_samples > coverage.samples / 3 && coverage.observed_samples * 100 < coverage.samples, "{coverage:?}");
}
#[test]
fn issue_4381_point_payload_is_validated_digested_and_budget_bounded() {
let mut cloud = Cloud::default();
cloud.sheet(Sheet { y: -0.001, x: [0., 1.], spacing: 0.01, noise: 0.001, color: RED }, true);
let (request, rgba) = point_request(&cloud, PointOrientation::TargetReferenced, 0.01);
let payload = cloud.payload(PointOrientation::TargetReferenced);
let bytes = THIN_WALL_IFC;
let a = plan_point_transfer(bytes.as_bytes(), &request, &rgba, &payload).unwrap();
let mut recoloured = cloud.clone();
recoloured.colors[0] = 7;
let b = plan_point_transfer(bytes.as_bytes(), &request, &rgba, &recoloured.payload(PointOrientation::TargetReferenced)).unwrap();
assert_ne!(a.transfer.prepared_sha256, b.transfer.prepared_sha256, "point bytes are bound into the digest");
for (orientation, payload) in [
(PointOrientation::SourceNormals, cloud.payload(PointOrientation::TargetReferenced)),
(PointOrientation::Viewpoints, cloud.payload(PointOrientation::TargetReferenced)),
(PointOrientation::TargetReferenced, cloud.payload(PointOrientation::SourceNormals)),
(PointOrientation::TargetReferenced, cloud.payload(PointOrientation::Viewpoints)),
] {
let (mismatched, rgba) = point_request(&cloud, orientation, 0.01);
assert!(plan_point_transfer(bytes.as_bytes(), &mismatched, &rgba, &payload).unwrap_err().contains("orientation"), "{orientation:?}");
}
let mut short = request.clone();
if let TransferSource::Points(spec) = &mut short.source { spec.point_count -= 1; }
assert!(plan_point_transfer(bytes.as_bytes(), &short, &rgba, &payload).unwrap_err().contains("point payload"));
let mut wide = request.clone();
if let TransferSource::Points(spec) = &mut wide.source { spec.surface_band_metres = 1.; }
assert!(plan_point_transfer(bytes.as_bytes(), &wide, &rgba, &payload).unwrap_err().contains("surface band"));
let mut imaged = request.clone();
imaged.source_image = Some(AppearanceRaster { width: 1, height: 1, byte_offset: 0, byte_length: 4 });
assert!(plan_point_transfer(bytes.as_bytes(), &imaged, &[255; 4], &payload).unwrap_err().contains("per point"));
assert!(plan_mesh_transfer(bytes.as_bytes(), &request, &rgba).unwrap_err().contains("binary point payload"));
let mut stale = request.clone();
stale.registration_sha256 = "0".repeat(64);
assert!(plan_point_transfer(bytes.as_bytes(), &stale, &rgba, &payload).unwrap_err().contains("digest"));
let unchanged = register_scan_correspondences(&request.registration).unwrap().request_sha256;
assert_eq!(unchanged, request.registration_sha256);
let (spec, frame) = (match &request.source { TransferSource::Points(spec) => spec.clone(), _ => unreachable!() }, identity());
let mut budget = TransferBudget::new();
let mut surface = PointSurface::new(&request, &spec, &payload, &frame, &mut budget).unwrap();
let targets = [TargetTriangle { points: [[0., 0., 0.], [1., 0., 0.], [1., 0., 1.]], normal: [0., -1., 0.], area: 0.5 }];
let mut occluder = Occluder::new(&targets, &mut budget).unwrap();
assert_eq!(surface.observe([0.5, 0., 0.5], [0., -1., 0.], &mut occluder, &mut budget).unwrap().0, Observation::Observed);
budget.work = 2;
assert!(surface.observe([0.5, 0., 0.5], [0., -1., 0.], &mut occluder, &mut budget).unwrap_err().contains("budget"));
}
#[test]
fn issue_4381_request_source_is_a_tagged_union_with_denied_unknown_fields() {
let mut cloud = Cloud::default();
cloud.sheet(Sheet { y: -0.001, x: [0., 1.], spacing: 0.05, noise: 0., color: RED }, true);
let (request, _) = point_request(&cloud, PointOrientation::Viewpoints, 0.01);
let json = serde_json::to_value(&request).unwrap();
assert_eq!(json["source"]["kind"], "points");
assert_eq!(json["source"]["orientation"], "viewpoints");
assert!(json["source"]["pointCount"].is_number() && json.get("sourceImage").is_none_or(|v| v.is_null()));
let (mesh_request, _) = control(mesh(), 0.02, 0.01);
let mesh_json = serde_json::to_value(&mesh_request).unwrap();
assert_eq!(mesh_json["source"]["kind"], "mesh");
assert!(mesh_json["source"]["positions"].is_array() && mesh_json["sourceImage"]["width"] == 3);
let back: MeshTransferRequest = serde_json::from_value(json.clone()).unwrap();
assert!(matches!(back.source, TransferSource::Points(_)));
let mut unknown = json.clone();
unknown["source"]["gaussianRadius"] = serde_json::json!(0.1);
assert!(serde_json::from_value::<MeshTransferRequest>(unknown).is_err(), "unknown point fields are refused");
let mut untagged = json;
untagged["source"].as_object_mut().unwrap().remove("kind");
assert!(serde_json::from_value::<MeshTransferRequest>(untagged).is_err());
}