use crate::section::{self, Cut};
const PINCH: f64 = 1e-9;
#[derive(Clone, Debug, PartialEq, Default)]
pub struct Curve {
pub spans: Vec<Vec<[f64; 2]>>,
pub notes: Vec<String>,
}
pub fn curve(cuts: &[Cut]) -> Curve {
let chains = section::chain(cuts)
.into_iter()
.map(|chain| (chain, Vec::new()))
.collect();
curve_of_chains(chains).curve
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct SourcedCurve {
pub curve: Curve,
pub sources: Vec<Vec<section::CutEnd>>,
}
pub fn curve_sourced(cuts: &[section::SourcedCut]) -> SourcedCurve {
let chains = section::chain_sourced(cuts)
.into_iter()
.map(|one| (one.chain, one.sources))
.collect();
curve_of_chains(chains)
}
fn curve_of_chains(chains: Vec<(section::Chain, Vec<section::CutEnd>)>) -> SourcedCurve {
let mut out = Curve::default();
let mut kept: Vec<(Vec<[f64; 2]>, Vec<section::CutEnd>)> = Vec::new();
for (at, (chain, mut sources)) in chains.into_iter().enumerate() {
if chain.profile.len() < 2 {
continue;
}
let mut samples = chain.profile.clone();
if samples.first().map(|p| p[0]) > samples.last().map(|p| p[0]) {
samples.reverse();
sources.reverse();
}
if !samples.windows(2).all(|pair| pair[1][0] >= pair[0][0]) {
out.notes.push(format!(
"span {at}: the abscissa is not monotonic - the surface folds back along this \
trace, and z is not a function of s there"
));
continue;
}
kept.push((samples, sources));
}
kept.sort_by(|a, b| a.0[0][0].total_cmp(&b.0[0][0]));
let mut sourced = Vec::with_capacity(kept.len());
for (samples, sources) in kept {
out.spans.push(samples);
sourced.push(sources);
}
SourcedCurve {
curve: out,
sources: sourced,
}
}
fn z_at(span: &[[f64; 2]], s: f64) -> Option<f64> {
let (first, last) = (span.first()?, span.last()?);
if s < first[0] || s > last[0] {
return None;
}
for pair in span.windows(2) {
let ([s0, z0], [s1, z1]) = (pair[0], pair[1]);
if s >= s0 && s <= s1 {
let span_s = s1 - s0;
if span_s.abs() < f64::EPSILON {
return Some(z0);
}
return Some(z0 + (z1 - z0) * (s - s0) / span_s);
}
}
None
}
#[derive(Clone, Debug, PartialEq)]
pub struct Patch {
pub points: Vec<[f64; 3]>,
pub triangles: Vec<[u32; 3]>,
pub profile: Vec<[f64; 2]>,
pub inverted: bool,
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct Panel {
pub patches: Vec<Patch>,
pub notes: Vec<String>,
}
impl Panel {
pub fn is_empty(&self) -> bool {
self.patches.is_empty()
}
}
pub fn panel(upper: &Curve, lower: &Curve, trace: &[[f64; 2]]) -> Panel {
let mut out = Panel::default();
for (i, up) in upper.spans.iter().enumerate() {
for (j, low) in lower.spans.iter().enumerate() {
let from = up[0][0].max(low[0][0]);
let to = up[up.len() - 1][0].min(low[low.len() - 1][0]);
if !matches!(to.partial_cmp(&from), Some(std::cmp::Ordering::Greater)) {
continue;
}
let mut steps: Vec<f64> = up
.iter()
.chain(low.iter())
.map(|p| p[0])
.filter(|s| *s >= from && *s <= to)
.collect();
steps.push(from);
steps.push(to);
steps.sort_by(f64::total_cmp);
steps.dedup_by(|a, b| (*a - *b).abs() <= f64::EPSILON * a.abs().max(1.0));
let mut samples: Vec<(f64, f64, f64)> = Vec::with_capacity(steps.len());
for s in steps {
let (Some(zu), Some(zl)) = (z_at(up, s), z_at(low, s)) else {
continue;
};
samples.push((s, zu, zl));
}
if samples.len() < 2 {
continue;
}
let scale = samples
.iter()
.map(|(_, zu, zl)| zu.abs().max(zl.abs()))
.fold(1.0_f64, f64::max);
let crossed = build(&mut out, &samples, trace, scale * PINCH);
if crossed > 0 {
out.notes.push(format!(
"spans {i}/{j}: {crossed} crossing(s) - the upper surface passes below the \
lower one, which only a human can explain (erosion, extension, throw, wrong \
data)"
));
}
}
}
out.notes.extend(upper.notes.iter().cloned());
out.notes.extend(lower.notes.iter().cloned());
out
}
fn build(out: &mut Panel, samples: &[(f64, f64, f64)], trace: &[[f64; 2]], pinch: f64) -> usize {
let mut crossed = 0usize;
let mut current: Vec<(f64, f64, f64)> = Vec::new();
let mut sign: Option<bool> = None;
let sign_of = |zu: f64, zl: f64| -> Option<bool> {
let thickness = zu - zl;
if thickness.abs() <= pinch {
None
} else {
Some(thickness > 0.0)
}
};
for (at, sample) in samples.iter().enumerate() {
let here = sign_of(sample.1, sample.2);
match (sign, here) {
(None, Some(side)) => {
sign = Some(side);
current.push(*sample);
}
(_, None) => current.push(*sample),
(Some(was), Some(side)) if was == side => current.push(*sample),
(Some(was), Some(side)) => {
debug_assert_ne!(was, side);
crossed += 1;
let previous = samples[at - 1];
let meeting = crossing(previous, *sample);
current.push(meeting);
flush(out, &mut current, trace, was);
current.push(meeting);
current.push(*sample);
sign = Some(side);
}
}
}
flush(out, &mut current, trace, sign.unwrap_or(true));
crossed
}
fn crossing(a: (f64, f64, f64), b: (f64, f64, f64)) -> (f64, f64, f64) {
let (s0, zu0, zl0) = a;
let (s1, zu1, zl1) = b;
let d0 = zu0 - zl0;
let d1 = zu1 - zl1;
let step = d0 - d1;
if step.abs() < f64::EPSILON {
return b;
}
let t = (d0 / step).clamp(0.0, 1.0);
let s = s0 + (s1 - s0) * t;
let z = zu0 + (zu1 - zu0) * t;
(s, z, z)
}
fn flush(out: &mut Panel, current: &mut Vec<(f64, f64, f64)>, trace: &[[f64; 2]], upright: bool) {
let samples = std::mem::take(current);
if samples.len() < 2 {
return;
}
let mut points = Vec::with_capacity(samples.len() * 2);
let mut profile = Vec::with_capacity(samples.len() * 2);
for (s, zu, zl) in &samples {
let [x, y] = point_at(trace, *s);
points.push([x, y, *zu]);
points.push([x, y, *zl]);
profile.push([*s, *zu]);
profile.push([*s, *zl]);
}
let mut triangles = Vec::with_capacity((samples.len() - 1) * 2);
for at in 0..samples.len() - 1 {
let (a, b) = ((at * 2) as u32, (at * 2 + 2) as u32);
let left_flat = (samples[at].1 - samples[at].2).abs() <= f64::EPSILON;
let right_flat = (samples[at + 1].1 - samples[at + 1].2).abs() <= f64::EPSILON;
if !left_flat {
triangles.push([a, a + 1, b]);
}
if !right_flat {
triangles.push([a + 1, b + 1, b]);
}
}
if triangles.is_empty() {
return;
}
out.patches.push(Patch {
points,
triangles,
profile,
inverted: !upright,
});
}
pub fn point_at(trace: &[[f64; 2]], s: f64) -> [f64; 2] {
let Some(first) = trace.first() else {
return [0.0, 0.0];
};
if s <= 0.0 {
return *first;
}
let mut travelled = 0.0;
for pair in trace.windows(2) {
let ([x0, y0], [x1, y1]) = (pair[0], pair[1]);
let length = (x1 - x0).hypot(y1 - y0);
if length < 1e-12 {
continue;
}
if s <= travelled + length {
let t = (s - travelled) / length;
return [x0 + (x1 - x0) * t, y0 + (y1 - y0) * t];
}
travelled += length;
}
*trace.last().unwrap_or(first)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Crossline {
pub s: f64,
pub trace: [f64; 4],
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Crosslines {
pub lines: Vec<Crossline>,
pub notes: Vec<String>,
}
pub fn crosslines(trace: &[[f64; 2]], spacing: f64, half_width: f64) -> Result<Crosslines, String> {
if !spacing.is_finite() || spacing <= 0.0 {
return Err(format!("a spacing of {spacing} places no crossline"));
}
if !half_width.is_finite() || half_width <= 0.0 {
return Err(format!("a half-width of {half_width} cuts nothing"));
}
let mut segments: Vec<([f64; 2], [f64; 2], f64)> = Vec::new();
let mut total = 0.0;
for pair in trace.windows(2) {
let (a, b) = (pair[0], pair[1]);
if !a.iter().chain(b.iter()).all(|v| v.is_finite()) {
return Err(
"the trace carries a point with no position - nothing is perpendicular \
to it"
.into(),
);
}
let length = (b[0] - a[0]).hypot(b[1] - a[1]);
if length < 1e-12 {
continue;
}
segments.push((a, b, length));
total += length;
}
if segments.is_empty() {
return Err("the trace has no two distinct points".into());
}
let mut abscissas: Vec<f64> = Vec::new();
let mut at = 0.0;
while at < total {
abscissas.push(at);
at += spacing;
}
match abscissas.last() {
Some(&last) if total - last < spacing / 2.0 && abscissas.len() > 1 => {
abscissas.pop();
}
_ => {}
}
abscissas.push(total);
let lines: Vec<Crossline> = abscissas
.iter()
.map(|&s| {
let centre = point_at(trace, s);
let [tx, ty] = tangent_at(&segments, s);
let (nx, ny) = (-ty, tx);
Crossline {
s,
trace: [
centre[0] - nx * half_width,
centre[1] - ny * half_width,
centre[0] + nx * half_width,
centre[1] + ny * half_width,
],
}
})
.collect();
for (at, one) in lines.iter().enumerate() {
for next in &lines[at + 1..] {
let Some(reach) = crossing_reach(&one.trace, &next.trace) else {
continue;
};
return Err(format!(
"the crosslines at s = {:.3} and s = {:.3} run through each other: the trace \
passes within {:.3} of itself there. A half-width under {:.3} would pass.",
one.s, next.s, reach, reach
));
}
}
let mut notes = Vec::new();
if lines.len() < 2 {
notes.push(format!(
"only one crossline: the trace measures {total:.3} for a spacing of {spacing}"
));
}
Ok(Crosslines { lines, notes })
}
fn tangent_at(segments: &[([f64; 2], [f64; 2], f64)], s: f64) -> [f64; 2] {
let mut travelled = 0.0;
let mut last = [1.0, 0.0];
for &(a, b, length) in segments {
last = [(b[0] - a[0]) / length, (b[1] - a[1]) / length];
if s <= travelled + length {
return last;
}
travelled += length;
}
last
}
fn crossing_reach(one: &[f64; 4], two: &[f64; 4]) -> Option<f64> {
let (p, r) = ([one[0], one[1]], [one[2] - one[0], one[3] - one[1]]);
let (q, s) = ([two[0], two[1]], [two[2] - two[0], two[3] - two[1]]);
let centre = [(one[0] + one[2]) / 2.0, (one[1] + one[3]) / 2.0];
let other = [(two[0] + two[2]) / 2.0, (two[1] + two[3]) / 2.0];
let denominator = r[0] * s[1] - r[1] * s[0];
let (dx, dy) = (q[0] - p[0], q[1] - p[1]);
if denominator.abs() < 1e-15 {
let off_line = dx * r[1] - dy * r[0];
if off_line.abs() > 1e-12 * r[0].hypot(r[1]).max(1.0) {
return None;
}
let apart = (other[0] - centre[0]).hypot(other[1] - centre[1]);
let half = r[0].hypot(r[1]) / 2.0;
return (apart < half * 2.0).then_some(apart / 2.0);
}
let t = (dx * s[1] - dy * s[0]) / denominator;
let u = (dx * r[1] - dy * r[0]) / denominator;
if !(0.0..=1.0).contains(&t) || !(0.0..=1.0).contains(&u) {
return None;
}
let hit = [p[0] + r[0] * t, p[1] + r[1] * t];
Some((hit[0] - centre[0]).hypot(hit[1] - centre[1]))
}
#[cfg(test)]
mod tests {
use super::*;
fn flat(z: f64) -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
(
vec![[0.0, 0.0, z], [10.0, 0.0, z], [10.0, 10.0, z], [0.0, 10.0, z]],
vec![[0, 1, 2], [0, 2, 3]],
)
}
fn sloped(at_zero: f64, at_ten: f64) -> (Vec<[f64; 3]>, Vec<[u32; 3]>) {
(
vec![
[0.0, 0.0, at_zero],
[10.0, 0.0, at_ten],
[10.0, 10.0, at_ten],
[0.0, 10.0, at_zero],
],
vec![[0, 1, 2], [0, 2, 3]],
)
}
fn trace() -> Vec<[f64; 2]> {
vec![[0.0, 5.0], [10.0, 5.0]]
}
fn curve_of(surface: (Vec<[f64; 3]>, Vec<[u32; 3]>)) -> Curve {
curve(§ion::cut(&surface.0, &surface.1, &trace()))
}
#[test]
fn two_flat_surfaces_make_one_slab_of_the_right_thickness() {
let panel = panel(&curve_of(flat(100.0)), &curve_of(flat(50.0)), &trace());
assert_eq!(panel.patches.len(), 1, "{:?}", panel.notes);
assert!(panel.notes.is_empty(), "{:?}", panel.notes);
let patch = &panel.patches[0];
assert!(!patch.inverted);
for point in &patch.points {
assert!((50.0..=100.0).contains(&point[2]), "{point:?}");
assert!((point[1] - 5.0).abs() < 1e-9, "{point:?}");
assert!((0.0..=10.0).contains(&point[0]), "{point:?}");
}
assert!(
patch
.triangles
.iter()
.flatten()
.all(|i| (*i as usize) < patch.points.len())
);
}
#[test]
fn a_wedge_pinches_out_without_a_word() {
let panel = panel(&curve_of(sloped(100.0, 50.0)), &curve_of(flat(50.0)), &trace());
assert_eq!(panel.patches.len(), 1, "{:?}", panel.notes);
assert!(
panel.notes.is_empty(),
"a wedge is not an anomaly: {:?}",
panel.notes
);
assert!(!panel.patches[0].inverted);
let patch = &panel.patches[0];
for pair in patch.profile.chunks(2) {
assert!(pair[0][1] - pair[1][1] >= -1e-9, "{pair:?}");
}
}
#[test]
fn a_crossing_splits_the_panel_and_marks_the_inverted_half() {
let panel = panel(
&curve_of(sloped(100.0, 0.0)),
&curve_of(sloped(0.0, 100.0)),
&trace(),
);
assert_eq!(panel.patches.len(), 2, "{:?}", panel.notes);
assert!(!panel.patches[0].inverted);
assert!(panel.patches[1].inverted, "the inverted half must say so");
assert!(
panel.notes.iter().any(|note| note.contains("crossing")),
"{:?}",
panel.notes
);
let end = panel.patches[0].profile.last().expect("a last point");
assert!((end[0] - 5.0).abs() < 1e-9, "{end:?}");
assert!((end[1] - 50.0).abs() < 1e-9, "{end:?}");
}
#[test]
fn a_hole_in_a_surface_ends_the_patch_rather_than_bridging_it() {
let points = vec![
[0.0, 0.0, 100.0],
[3.0, 0.0, 100.0],
[3.0, 10.0, 100.0],
[0.0, 10.0, 100.0],
[7.0, 0.0, 100.0],
[10.0, 0.0, 100.0],
[10.0, 10.0, 100.0],
[7.0, 10.0, 100.0],
];
let triangles = vec![[0, 1, 2], [0, 2, 3], [4, 5, 6], [4, 6, 7]];
let upper = curve(§ion::cut(&points, &triangles, &trace()));
assert_eq!(upper.spans.len(), 2, "two spans, not one");
let panel = panel(&upper, &curve_of(flat(50.0)), &trace());
assert_eq!(panel.patches.len(), 2);
for patch in &panel.patches {
for point in &patch.points {
assert!(point[0] <= 3.0 + 1e-9 || point[0] >= 7.0 - 1e-9, "{point:?}");
}
}
}
#[test]
fn a_surface_with_no_cut_gives_an_empty_panel_not_a_failure() {
let away = vec![[100.0, 100.0], [110.0, 100.0]];
let upper = curve(§ion::cut(&flat(100.0).0, &flat(100.0).1, &away));
let panel = panel(&upper, &curve_of(flat(50.0)), &away);
assert!(panel.is_empty());
}
#[test]
fn a_bent_trace_places_its_points_on_the_bend() {
let bent = vec![[0.0, 0.0], [5.0, 0.0], [5.0, 5.0]];
assert_eq!(point_at(&bent, 0.0), [0.0, 0.0]);
assert_eq!(point_at(&bent, 2.5), [2.5, 0.0]);
assert_eq!(point_at(&bent, 5.0), [5.0, 0.0]);
assert_eq!(point_at(&bent, 7.5), [5.0, 2.5]);
assert_eq!(point_at(&bent, 99.0), [5.0, 5.0]);
assert_eq!(point_at(&bent, -3.0), [0.0, 0.0]);
}
#[test]
fn a_fold_is_refused_by_name_rather_than_twisted_into_a_panel() {
let cuts = vec![
Cut {
ends: [[0.0, 5.0, 10.0], [5.0, 5.0, 20.0]],
profile: [[0.0, 10.0], [5.0, 20.0]],
},
Cut {
ends: [[5.0, 5.0, 20.0], [2.0, 5.0, 30.0]],
profile: [[5.0, 20.0], [2.0, 30.0]],
},
];
let made = curve(&cuts);
assert!(made.spans.is_empty());
assert_eq!(made.notes.len(), 1);
assert!(made.notes[0].contains("monotonic"), "{:?}", made.notes);
}
fn ell() -> Vec<[f64; 2]> {
vec![[0.0, 0.0], [10.0, 0.0], [10.0, 10.0]]
}
#[test]
fn the_profiles_are_spaced_in_arc_length_and_reach_the_end() {
let made = crosslines(&ell(), 5.0, 2.0).expect("an L at half-width 2 passes");
let at: Vec<f64> = made.lines.iter().map(|one| one.s).collect();
assert_eq!(at, vec![0.0, 5.0, 10.0, 15.0, 20.0]);
}
#[test]
fn the_last_profile_does_not_double_the_end() {
let almost = vec![[0.0, 0.0], [10.4, 0.0]];
let made = crosslines(&almost, 5.0, 1.0).expect("straight, so nothing crosses");
let at: Vec<f64> = made.lines.iter().map(|one| one.s).collect();
assert_eq!(at, vec![0.0, 5.0, 10.4]);
}
#[test]
fn half_width_is_a_half_and_the_profile_is_perpendicular() {
let straight = vec![[0.0, 0.0], [10.0, 0.0]];
let made = crosslines(&straight, 5.0, 3.0).expect("straight");
let first = made.lines[0];
let [x0, y0, x1, y1] = first.trace;
assert!((x1 - x0).hypot(y1 - y0) - 6.0 < 1e-12, "the length is 2 × 3");
assert!((x0 - 0.0).abs() < 1e-12 && (x1 - 0.0).abs() < 1e-12);
assert!((y0 + y1).abs() < 1e-12, "centred on the trace");
}
#[test]
fn a_bend_tighter_than_the_half_width_is_refused_naming_a_half_width_that_would_pass() {
let why = crosslines(&ell(), 5.0, 6.0).expect_err("6 > 5: it runs through itself");
assert!(why.contains("5.000") && why.contains("15.000"), "{why}");
assert!(why.contains("A half-width under 5.000"), "{why}");
assert!(crosslines(&ell(), 5.0, 4.9).is_ok());
}
#[test]
fn a_trace_folding_back_on_itself_is_caught_even_where_neighbours_are_tame() {
let hairpin = vec![[0.0, 0.0], [20.0, 0.0], [20.0, 4.0], [0.0, 4.0]];
let why = crosslines(&hairpin, 5.0, 3.0).expect_err("the two branches are 4 apart");
assert!(why.contains("A half-width under 2.000"), "{why}");
assert!(crosslines(&hairpin, 5.0, 1.5).is_ok());
}
#[test]
fn what_places_no_profile_is_refused_by_name_rather_than_returning_none() {
let straight = vec![[0.0, 0.0], [10.0, 0.0]];
assert!(crosslines(&straight, 0.0, 1.0).is_err());
assert!(crosslines(&straight, f64::NAN, 1.0).is_err());
assert!(crosslines(&straight, 5.0, -1.0).is_err());
assert!(crosslines(&[[0.0, 0.0]], 5.0, 1.0).is_err());
assert!(crosslines(&[[3.0, 3.0], [3.0, 3.0]], 5.0, 1.0).is_err());
assert!(crosslines(&[[0.0, 0.0], [f64::NAN, 1.0]], 5.0, 1.0).is_err());
}
}