1use crate::index::{ProjectionMode, TriangleIndex};
15use crate::mesh::MeshSnapshot;
16use crate::model::{Bounds3, Point3};
17use crate::stock_sim::DexelGrid;
18use serde::{Deserialize, Serialize};
19
20#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23#[serde(default)]
24pub struct DeviationOptions {
25 pub tolerance: f64,
27 pub max_distance: f64,
30}
31
32impl Default for DeviationOptions {
33 fn default() -> Self {
34 Self { tolerance: 0.05, max_distance: 1_000.0 }
35 }
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
40#[serde(rename_all = "kebab-case")]
41pub enum DeviationClass {
42 Within,
43 Excess,
44 Gouge,
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
49#[serde(rename_all = "kebab-case")]
50pub enum ColumnPlacement {
51 AbovePart,
54 WithinExtent,
57 BelowPart,
62}
63
64#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub struct FaceDeviation {
68 pub face: u32,
70 pub solid: String,
71 pub face_name: String,
72 pub samples: u64,
73 pub min: f64,
74 pub max: f64,
75 pub mean: f64,
76 pub gouged: u64,
77 pub excess: u64,
78}
79
80#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct DeviationMap {
84 pub origin: Point3,
85 pub cell_x: f64,
86 pub cell_y: f64,
87 pub nx: u32,
88 pub ny: u32,
89 pub values: Vec<Option<f64>>,
92 pub heights: Vec<Option<f64>>,
94 pub faces_of: Vec<Option<u32>>,
96 pub faces: Vec<FaceDeviation>,
97 pub tolerance: f64,
98 pub min: f64,
99 pub max: f64,
100 pub within: u64,
101 pub excess: u64,
102 pub gouged: u64,
103 #[serde(default)]
106 pub placement: Vec<Option<ColumnPlacement>>,
107 #[serde(default)]
110 pub below_part: u64,
111 #[serde(default)]
112 pub below_part_volume: f64,
113}
114
115impl DeviationMap {
116 pub fn classify(&self, deviation: f64) -> DeviationClass {
117 if deviation.abs() <= self.tolerance {
118 DeviationClass::Within
119 } else if deviation > 0.0 {
120 DeviationClass::Excess
121 } else {
122 DeviationClass::Gouge
123 }
124 }
125
126 pub fn value(&self, ix: u32, iy: u32) -> Option<f64> {
127 self.values[(iy * self.nx + ix) as usize]
128 }
129
130 pub fn value_at(&self, x: f64, y: f64) -> Option<f64> {
132 let ix = ((x - self.origin.x) / self.cell_x).floor();
133 let iy = ((y - self.origin.y) / self.cell_y).floor();
134 if ix < 0.0 || iy < 0.0 || ix >= self.nx as f64 || iy >= self.ny as f64 {
135 return None;
136 }
137 self.value(ix as u32, iy as u32)
138 }
139
140 pub fn face(&self, name: &str) -> Option<&FaceDeviation> {
141 self.faces.iter().find(|f| f.face_name == name)
142 }
143}
144
145pub use crate::geom::closest_point_on_triangle;
147
148pub use crate::geom::point_triangle_distance_squared;
150
151pub(crate) fn part_bottom_beside(part: &MeshSnapshot, index: &dyn TriangleIndex, p: Point3, q: Point3, step: f64, scratch: &mut Vec<u32>) -> Option<f64> {
155 let (dx, dy) = (q.x - p.x, q.y - p.y);
156 let len = (dx * dx + dy * dy).sqrt();
157 let (x, y) = if len > 0.0 { (q.x + dx / len * step, q.y + dy / len * step) } else { (q.x, q.y) };
158 part_bottom_under(part, index, x, y, scratch)
159}
160
161pub fn nearest_triangle(part: &MeshSnapshot, index: &dyn TriangleIndex, p: Point3, start_radius: f64, max_distance: f64) -> Option<(u32, f64)> {
165 let mut radius = start_radius.max(f64::MIN_POSITIVE);
166 let mut candidates = Vec::new();
167 loop {
168 let q = Bounds3::from_point(p).expanded(radius);
169 candidates.clear();
170 index.query_aabb(&q, ProjectionMode::Xyz, &mut candidates);
171 let mut best: Option<(u32, f64)> = None;
172 for &id in &candidates {
173 let t = part.triangle(id);
174 let d2 = point_triangle_distance_squared(p, t.a, t.b, t.c);
175 if best.map_or(true, |(_, d)| d2 < d * d) {
176 best = Some((id, d2.sqrt()));
177 }
178 }
179 if let Some((id, d)) = best {
181 if d <= radius {
182 return Some((id, d));
183 }
184 }
185 if radius >= max_distance {
186 return best.filter(|(_, d)| *d <= max_distance);
187 }
188 radius = (radius * 2.0).min(max_distance);
189 }
190}
191
192pub(crate) fn inside_part(part: &MeshSnapshot, index: &dyn TriangleIndex, p: Point3, scratch: &mut Vec<u32>) -> bool {
195 scratch.clear();
196 let q = Bounds3::new(Point3::new(p.x, p.y, f64::NEG_INFINITY), Point3::new(p.x, p.y, f64::INFINITY));
197 index.query_aabb(&q, ProjectionMode::Xy, scratch);
198 let mut hits: Vec<f64> = scratch
199 .iter()
200 .filter_map(|&id| {
201 let t = part.triangle(id);
202 crate::stock_sim::vertical_ray_hit(t.a, t.b, t.c, p.x, p.y)
203 })
204 .collect();
205 hits.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
206 hits.dedup();
207 hits.iter().filter(|&&z| z > p.z).count() % 2 == 1
208}
209
210pub(crate) fn part_bottom_under(part: &MeshSnapshot, index: &dyn TriangleIndex, x: f64, y: f64, scratch: &mut Vec<u32>) -> Option<f64> {
212 scratch.clear();
213 let q = Bounds3::new(Point3::new(x, y, f64::NEG_INFINITY), Point3::new(x, y, f64::INFINITY));
214 index.query_aabb(&q, ProjectionMode::Xy, scratch);
215 scratch
216 .iter()
217 .filter_map(|&id| {
218 let t = part.triangle(id);
219 crate::stock_sim::vertical_ray_hit(t.a, t.b, t.c, x, y)
220 })
221 .fold(None, |m: Option<f64>, z| Some(m.map_or(z, |m| m.min(z))))
222}
223
224fn part_top_under(part: &MeshSnapshot, index: &dyn TriangleIndex, x: f64, y: f64, scratch: &mut Vec<u32>) -> Option<(f64, u32)> {
226 scratch.clear();
227 let q = Bounds3::new(Point3::new(x, y, f64::NEG_INFINITY), Point3::new(x, y, f64::INFINITY));
228 index.query_aabb(&q, ProjectionMode::Xy, scratch);
229 let mut best: Option<(f64, u32)> = None;
230 for &id in scratch.iter() {
231 let t = part.triangle(id);
232 if let Some(z) = crate::stock_sim::vertical_ray_hit(t.a, t.b, t.c, x, y) {
233 if best.map_or(true, |(bz, _)| z > bz) {
234 best = Some((z, t.face));
235 }
236 }
237 }
238 best
239}
240
241#[derive(Clone, Debug, PartialEq)]
245pub struct ExcessSample {
246 pub point: Point3,
248 pub excess: f64,
250 pub face: u32,
252 pub nearest: Point3,
254}
255
256pub fn excess_samples(map: &DeviationMap, part: &MeshSnapshot, index: &dyn TriangleIndex, options: &DeviationOptions) -> Vec<ExcessSample> {
262 let start_radius = map.cell_x.max(map.cell_y);
263 let mut out = Vec::new();
264 for iy in 0..map.ny {
265 for ix in 0..map.nx {
266 let k = (iy * map.nx + ix) as usize;
267 let (Some(excess), Some(z), Some(face)) = (map.values[k], map.heights[k], map.faces_of[k]) else { continue };
268 if excess <= map.tolerance {
269 continue;
270 }
271 let point = Point3::new(map.origin.x + (ix as f64 + 0.5) * map.cell_x, map.origin.y + (iy as f64 + 0.5) * map.cell_y, z);
272 let Some((tri, _)) = nearest_triangle(part, index, point, start_radius, options.max_distance) else { continue };
274 let t = part.triangle(tri);
275 out.push(ExcessSample { point, excess, face, nearest: closest_point_on_triangle(point, t.a, t.b, t.c) });
276 }
277 }
278 out
279}
280
281pub fn deviation_map(grid: &DexelGrid, part: &MeshSnapshot, index: &dyn TriangleIndex, options: &DeviationOptions) -> DeviationMap {
282 let n = grid.columns.len();
283 let mut values = vec![None; n];
284 let mut heights = vec![None; n];
285 let mut faces_of = vec![None; n];
286 let mut per_face: Vec<Option<FaceDeviation>> = vec![None; part.faces.len()];
287 let mut placement = vec![None; n];
288 let (mut below_part, mut below_part_volume) = (0u64, 0.0f64);
289 let part_bottom = part.bounds.min.z;
291 let (mut min, mut max) = (f64::INFINITY, f64::NEG_INFINITY);
292 let (mut within, mut excess, mut gouged) = (0u64, 0u64, 0u64);
293 let start_radius = grid.cell_x.max(grid.cell_y);
294 let mut scratch = Vec::new();
295 for iy in 0..grid.ny {
296 for ix in 0..grid.nx {
297 let k = (iy * grid.nx + ix) as usize;
298 let (x, y) = grid.center(ix, iy);
299 let column = grid.column(ix, iy);
300 let (z, empty) = match column.top() {
302 Some(top) => (top, false),
303 None => (grid.z_range[0], true),
304 };
305 let p = Point3::new(x, y, z);
306 let (deviation, face) = if empty {
307 match part_top_under(part, index, x, y, &mut scratch) {
309 Some((top, face)) if top > z => {
310 placement[k] = Some(ColumnPlacement::AbovePart);
311 (-(top - z), face)
312 }
313 _ => continue,
314 }
315 } else {
316 let local_bottom = part_bottom_under(part, index, x, y, &mut scratch);
325 let below = match local_bottom {
326 Some(b) => z < b - options.tolerance,
327 None => {
328 let beside = nearest_triangle(part, index, p, start_radius, options.max_distance).and_then(|(tri, _)| {
332 let t = part.triangle(tri);
333 let q = closest_point_on_triangle(p, t.a, t.b, t.c);
334 part_bottom_beside(part, index, p, q, 1e-3 * grid.cell_x.min(grid.cell_y), &mut scratch)
335 });
336 z <= beside.unwrap_or(part_bottom) + options.tolerance
337 }
338 };
339 if below {
340 placement[k] = Some(ColumnPlacement::BelowPart);
341 below_part += 1;
342 below_part_volume += column.height() * grid.cell_x * grid.cell_y;
343 continue;
344 }
345 placement[k] = Some(if local_bottom.is_some() { ColumnPlacement::AbovePart } else { ColumnPlacement::WithinExtent });
346 if let Some(b) = local_bottom {
351 if z <= b + options.tolerance {
352 if let Some((top, face)) = part_top_under(part, index, x, y, &mut scratch) {
353 if top > z + options.tolerance {
354 let deviation = -(top - z);
355 values[k] = Some(deviation);
356 heights[k] = Some(z);
357 faces_of[k] = Some(face);
358 min = min.min(deviation);
359 max = max.max(deviation);
360 gouged += 1;
361 let entry = per_face[face as usize].get_or_insert_with(|| FaceDeviation {
362 face,
363 solid: part.faces[face as usize].solid.clone(),
364 face_name: part.faces[face as usize].face.clone(),
365 samples: 0,
366 min: f64::INFINITY,
367 max: f64::NEG_INFINITY,
368 mean: 0.0,
369 gouged: 0,
370 excess: 0,
371 });
372 entry.samples += 1;
373 entry.min = entry.min.min(deviation);
374 entry.max = entry.max.max(deviation);
375 entry.mean += deviation;
376 entry.gouged += 1;
377 continue;
378 }
379 }
380 }
381 }
382 let inside = inside_part(part, index, p, &mut scratch);
383 let Some((tri, distance)) = nearest_triangle(part, index, p, start_radius, options.max_distance) else {
384 continue;
385 };
386 (if inside { -distance } else { distance }, part.triangle(tri).face)
387 };
388 values[k] = Some(deviation);
389 heights[k] = Some(z);
390 faces_of[k] = Some(face);
391 min = min.min(deviation);
392 max = max.max(deviation);
393 let class = if deviation.abs() <= options.tolerance {
394 within += 1;
395 DeviationClass::Within
396 } else if deviation > 0.0 {
397 excess += 1;
398 DeviationClass::Excess
399 } else {
400 gouged += 1;
401 DeviationClass::Gouge
402 };
403 let entry = per_face[face as usize].get_or_insert_with(|| FaceDeviation {
404 face,
405 solid: part.faces[face as usize].solid.clone(),
406 face_name: part.faces[face as usize].face.clone(),
407 samples: 0,
408 min: f64::INFINITY,
409 max: f64::NEG_INFINITY,
410 mean: 0.0,
411 gouged: 0,
412 excess: 0,
413 });
414 entry.samples += 1;
415 entry.min = entry.min.min(deviation);
416 entry.max = entry.max.max(deviation);
417 entry.mean += deviation;
418 match class {
419 DeviationClass::Gouge => entry.gouged += 1,
420 DeviationClass::Excess => entry.excess += 1,
421 DeviationClass::Within => {}
422 }
423 }
424 }
425 let faces: Vec<FaceDeviation> = per_face
426 .into_iter()
427 .flatten()
428 .map(|mut f| {
429 f.mean /= f.samples as f64;
430 f
431 })
432 .collect();
433 if min > max {
434 min = 0.0;
435 max = 0.0;
436 }
437 DeviationMap {
438 origin: grid.origin,
439 cell_x: grid.cell_x,
440 cell_y: grid.cell_y,
441 nx: grid.nx,
442 ny: grid.ny,
443 values,
444 heights,
445 faces_of,
446 faces,
447 tolerance: options.tolerance,
448 min,
449 max,
450 within,
451 excess,
452 gouged,
453 placement,
454 below_part,
455 below_part_volume,
456 }
457}
458