use crate::topology::{adaptive_coedge_error, EdgeRecord};
use crate::{MeasuredTolerance, NurbsCurve, NurbsSurface, OffsetEvaluator, OffsetNormal, Vec3};
const FIT_SAMPLES: usize = 15;
const FIT_OFFSET_U: f64 = 0.31;
const FIT_OFFSET_V: f64 = 0.43;
pub fn measure_edge_against_pcurve_image(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
band: f64,
) -> Result<MeasuredTolerance, String> {
let adaptive = adaptive_coedge_error(surface, pcurve, &edge.curve, edge, forward, band)?;
let spans = span_midpoint_error(surface, pcurve, edge, forward)?;
Ok(MeasuredTolerance::new(adaptive.max(spans), band))
}
pub fn span_midpoint_error(
surface: &NurbsSurface,
pcurve: &NurbsCurve,
edge: &EdgeRecord,
forward: bool,
) -> Result<f64, String> {
let span = edge.t1 - edge.t0;
if !span.is_finite() || span.abs() <= 0.0 {
return Ok(0.0);
}
let [q0, q1] = pcurve.domain()?;
let low = edge.t0.min(edge.t1);
let high = edge.t0.max(edge.t1);
let mut breaks: Vec<f64> = vec![low];
for &knot in &edge.curve.knots {
if knot > low && knot < high && knot > *breaks.last().unwrap_or(&low) {
breaks.push(knot);
}
}
breaks.push(high);
let mut worst = 0.0f64;
for pair in breaks.windows(2) {
let midpoint = (pair[0] + pair[1]) * 0.5;
if !(midpoint > pair[0] && midpoint < pair[1]) {
continue;
}
let fraction = if forward {
(midpoint - edge.t0) / span
} else {
(edge.t1 - midpoint) / span
};
let uv = pcurve.evaluate(q0 + (q1 - q0) * fraction)?;
let on_surface = surface.evaluate_extended(uv.x, uv.y)?;
let on_curve = edge.curve.evaluate(midpoint)?;
worst = worst.max(on_surface.sub(on_curve).length());
}
Ok(worst)
}
pub fn measure_surface_fit_against_pointwise_offset(
source: &NurbsSurface,
same_sense: bool,
fitted: &NurbsSurface,
distance: f64,
band: f64,
) -> Result<MeasuredTolerance, String> {
let evaluator = OffsetEvaluator::new(
"measure_offset_fit",
source,
OffsetNormal::FaceStable { same_sense },
);
let [u0, u1] = source.domain_u()?;
let [v0, v1] = source.domain_v()?;
let mut worst = 0.0f64;
for iu in 0..FIT_SAMPLES {
let u = u0 + (u1 - u0) * (iu as f64 + FIT_OFFSET_U) / FIT_SAMPLES as f64;
for iv in 0..FIT_SAMPLES {
let v = v0 + (v1 - v0) * (iv as f64 + FIT_OFFSET_V) / FIT_SAMPLES as f64;
let want = evaluator.at(u, v, -distance)?.point;
let got = fitted.evaluate(u, v)?;
worst = worst.max(got.sub(want).length());
}
}
Ok(MeasuredTolerance::new(worst, band))
}
pub fn vertex_endpoint_gap(vertex_point: Vec3, curve_end: Vec3) -> f64 {
curve_end.sub(vertex_point).length()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{make_cylinder_brep, offset_face_carrier_measured};
#[test]
fn span_midpoints_see_the_chord_sag_the_dyadic_sampler_aliases_away() {
let solid = make_cylinder_brep(
crate::Vec3::default(),
crate::Vec3::new(0.0, 0.0, 1.0),
500.0,
1200.0,
)
.expect("cylinder");
let cap = solid
.shells
.iter()
.flat_map(|shell| &shell.faces)
.find(|face| {
matches!(
face.surface.analytic(),
Some(crate::AnalyticSurface::Plane { .. })
)
})
.expect("a planar cap")
.id;
let carrier = offset_face_carrier_measured(&solid, cap, 50.0, 0.0).expect("carrier");
let surface = &carrier.face.surface;
let coedge = &carrier.face.loops[0].coedges[0];
let edge = carrier
.edges
.iter()
.find(|edge| edge.id == coedge.edge_id)
.expect("the rim edge");
let band = 1.0;
let aliased = adaptive_coedge_error(
surface,
&coedge.pcurve,
&edge.curve,
edge,
coedge.forward,
band,
)
.expect("adaptive");
let spans =
span_midpoint_error(surface, &coedge.pcurve, edge, coedge.forward).expect("spans");
let recorded =
measure_edge_against_pcurve_image(surface, &coedge.pcurve, edge, coedge.forward, band)
.expect("measured");
assert!(
aliased < 1e-9,
"the dyadic sampler is expected to alias here; if this fails the \
aliasing is gone and the span pass may be reconsidered (got {aliased:.3e})"
);
assert!(
spans > 2e-3,
"the span pass must see the real chord sag (got {spans:.3e})"
);
assert_eq!(
recorded.deviation(),
aliased.max(spans),
"the recorded deviation is the worse of the two passes"
);
}
#[test]
fn an_exact_affine_carrier_records_a_zero_surface_fit() {
let solid = crate::make_box_brep(crate::Vec3::default(), 20.0, 20.0, 4.0).expect("box");
let face = solid.shells[0].faces[0].id;
let carrier = offset_face_carrier_measured(&solid, face, 0.25, 0.0).expect("carrier");
let deviation = carrier.deviation.expect("measured");
assert_eq!(deviation.lane, crate::OffsetSurfaceLane::Affine);
assert_eq!(deviation.surface.expect("a fit record").deviation(), 0.0);
assert!(deviation.exceedances().is_empty());
assert!(crate::offset_face_carrier(&solid, face, 0.25, 0.0)
.expect("carrier")
.deviation
.is_none());
}
}