use super::*;
pub(super) const MAX_EDGE_SPANS: usize = 512;
pub(super) const MAX_REFINE_PASSES: usize = 12;
pub(super) const ANGULAR_TOL: f64 = 0.35;
pub(super) const ANGULAR_SAG_FLOOR: f64 = 0.25;
pub(super) const EDGE_ANGULAR_TOL: f64 = 0.35;
pub(super) fn deflection_to_chord(point: Vec3, a: Vec3, b: Vec3) -> f64 {
let ab = b.sub(a);
let length_squared = ab.dot(ab);
if length_squared <= 1e-30 {
return point.sub(a).length();
}
let t = (point.sub(a).dot(ab) / length_squared).clamp(0.0, 1.0);
point.sub(a.add(ab.scale(t))).length()
}
pub(super) fn edge_fractions(
edge: &EdgeRecord,
chord_tolerance: f64,
) -> Result<(Vec<f64>, bool), String> {
if edge.degenerate {
return Ok((vec![0.0, 1.0], false));
}
let evaluate = |fraction: f64| -> Result<Vec3, String> {
edge.curve
.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)
};
let angular_cos = EDGE_ANGULAR_TOL.cos();
let tangent = |fraction: f64| -> Result<Option<Vec3>, String> {
let parameter = edge.t0 + (edge.t1 - edge.t0) * fraction;
let (_, derivative) = edge.curve.deriv1(parameter)?;
Ok(derivative.normalized().ok())
};
let mut fractions = vec![0.0, 1.0];
let span = edge.t1 - edge.t0;
if span.abs() > 1e-12 && edge.curve.knots.len() > 2 * edge.curve.degree {
let interior =
&edge.curve.knots[edge.curve.degree..edge.curve.knots.len() - edge.curve.degree];
for &knot in interior {
let fraction = (knot - edge.t0) / span;
if fraction > 1e-9 && fraction < 1.0 - 1e-9 {
fractions.push(fraction);
}
}
fractions.sort_by(f64::total_cmp);
fractions.dedup_by(|a, b| (*a - *b).abs() <= 1e-9);
if fractions.len() > MAX_EDGE_SPANS {
let step = fractions.len() as f64 / MAX_EDGE_SPANS as f64;
let thinned: Vec<f64> = (0..MAX_EDGE_SPANS)
.map(|index| fractions[(index as f64 * step) as usize])
.chain(std::iter::once(1.0))
.collect();
fractions = thinned;
}
}
loop {
let mut refined = Vec::with_capacity(fractions.len() * 2);
let mut split_any = false;
for pair in fractions.windows(2) {
refined.push(pair[0]);
if fractions.len() <= MAX_EDGE_SPANS {
let middle = (pair[0] + pair[1]) * 0.5;
let curve_mid = evaluate(middle)?;
let chord_sags =
deflection_to_chord(curve_mid, evaluate(pair[0])?, evaluate(pair[1])?)
> chord_tolerance;
let tangent_swings = match (tangent(pair[0])?, tangent(pair[1])?) {
(Some(ta), Some(tb)) => ta.dot(tb) < angular_cos,
_ => false,
};
if chord_sags || tangent_swings {
refined.push(middle);
split_any = true;
}
}
}
refined.push(1.0);
fractions = refined;
if !split_any || fractions.len() > MAX_EDGE_SPANS {
break;
}
}
let capped = fractions.len() > MAX_EDGE_SPANS;
Ok((fractions, capped))
}
pub(super) struct EdgeSamples {
pub(super) fractions: Vec<f64>,
pub(super) positions: Vec<Vec3>,
pub(super) worst_sag: f64,
}
fn chart_crossing_fractions(
edge: &EdgeRecord,
atlas: &crate::sphere_chart::SphereAtlas,
fractions: &[f64],
) -> Result<Vec<f64>, String> {
if edge.degenerate || fractions.len() < 2 {
return Ok(Vec::new());
}
let at = |fraction: f64| -> Result<[f64; 3], String> {
let point = edge
.curve
.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)?;
Ok(atlas.axis_coordinates(point))
};
let mut coordinates = Vec::with_capacity(fractions.len());
for &fraction in fractions {
coordinates.push(at(fraction)?);
}
let scale = coordinates
.iter()
.flat_map(|x| x.iter().map(|value| value.abs()))
.fold(0.0, f64::max);
let significant = 1e-9 * scale;
let mut crossings = Vec::new();
for (i, j) in [(0usize, 1usize), (1, 2), (2, 0)] {
for combination in [1.0f64, -1.0] {
let value = |x: &[f64; 3]| x[i] - combination * x[j];
for window in 0..fractions.len() - 1 {
let (mut lo, mut hi) = (fractions[window], fractions[window + 1]);
let (mut low_value, high_value) =
(value(&coordinates[window]), value(&coordinates[window + 1]));
if (low_value > 0.0) == (high_value > 0.0) || low_value == high_value {
continue;
}
if low_value.abs().max(high_value.abs()) <= significant {
continue;
}
for _ in 0..60 {
let middle = 0.5 * (lo + hi);
if middle <= lo || middle >= hi {
break;
}
let middle_value = value(&at(middle)?);
if (middle_value > 0.0) == (low_value > 0.0) {
lo = middle;
low_value = middle_value;
} else {
hi = middle;
}
}
let root = 0.5 * (lo + hi);
let x = at(root)?;
let tied = x[i].abs().max(x[j].abs());
let third = x[3 - i - j].abs();
let scale = x[0].abs().max(x[1].abs()).max(x[2].abs());
if scale > 0.0 && tied > 0.0 && tied >= third - 1e-9 * scale {
crossings.push(root);
}
}
}
}
Ok(crossings)
}
pub(super) fn sample_all_edges(
solid: &BrepSolid,
chord_tolerance: f64,
) -> Result<HashMap<u64, EdgeSamples>, String> {
let vertex_points: FxHashMap<u64, Vec3> = solid
.vertices
.iter()
.map(|vertex| (vertex.id, vertex.point))
.collect();
let vertex_point = |id: u64| -> Result<Vec3, String> {
vertex_points
.get(&id)
.copied()
.ok_or_else(|| format!("watertight tessellation: missing vertex {id}"))
};
let mut chart_atlases: FxHashMap<u64, Vec<crate::sphere_chart::SphereAtlas>> =
FxHashMap::default();
for face in solid.shells.iter().flat_map(|shell| shell.faces.iter()) {
let Some(atlas) = crate::sphere_chart::SphereAtlas::of_surface(&face.surface) else {
continue;
};
for coedge in face.loops.iter().flat_map(|record| record.coedges.iter()) {
chart_atlases
.entry(coedge.edge_id)
.or_default()
.push(atlas);
}
}
let mut samples = HashMap::new();
for edge in &solid.edges {
let (mut fractions, capped) = edge_fractions(edge, chord_tolerance)?;
if let Some(atlases) = chart_atlases.get(&edge.id) {
let mut extra = Vec::new();
for atlas in atlases {
extra.extend(chart_crossing_fractions(edge, atlas, &fractions)?);
}
if !extra.is_empty() {
fractions.extend(extra);
fractions.sort_by(f64::total_cmp);
fractions.dedup_by(|a, b| (*a - *b).abs() <= 1e-9);
fractions.retain(|fraction| *fraction >= 0.0 && *fraction <= 1.0);
}
}
let fractions = fractions;
let start = vertex_point(edge.start_vertex_id)?;
let end = vertex_point(edge.end_vertex_id)?;
let mut positions = Vec::with_capacity(fractions.len());
for (index, fraction) in fractions.iter().enumerate() {
if index == 0 {
positions.push(start);
} else if index == fractions.len() - 1 {
positions.push(end);
} else if edge.degenerate {
positions.push(start);
} else {
positions.push(
edge.curve
.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)?,
);
}
}
let mut worst_sag = 0.0f64;
if capped && !edge.degenerate {
for index in 0..fractions.len() - 1 {
let middle = (fractions[index] + fractions[index + 1]) * 0.5;
let curve_mid = edge
.curve
.evaluate(edge.t0 + (edge.t1 - edge.t0) * middle)?;
worst_sag = worst_sag.max(deflection_to_chord(
curve_mid,
positions[index],
positions[index + 1],
));
}
}
samples.insert(
edge.id,
EdgeSamples {
fractions,
positions,
worst_sag,
},
);
}
Ok(samples)
}
#[derive(Clone, Copy)]
pub(super) struct FaceVertex {
pub(super) uv: [f64; 2],
pub(super) position: Vec3,
}