use super::*;
use std::sync::Arc;
const CURVE_SAMPLES: usize = 256;
const CURVE_NEWTON_STEPS: usize = 24;
#[derive(Debug)]
pub(super) struct SampledCurve {
curve: crate::NurbsCurve,
t0: f64,
t1: f64,
closed: bool,
length: f64,
samples: Vec<(f64, Vec3)>,
}
impl SampledCurve {
pub(super) fn new(mate: &MateCurve, name: &str) -> Result<Self, String> {
let (t0, t1) = (mate.t0.min(mate.t1), mate.t0.max(mate.t1));
if !(t0.is_finite() && t1.is_finite() && t1 > t0) {
return Err(format!("{name}: curve parameter range [{}, {}] is empty", mate.t0, mate.t1));
}
let mut samples = Vec::with_capacity(CURVE_SAMPLES + 1);
for i in 0..=CURVE_SAMPLES {
let t = t0 + (t1 - t0) * i as f64 / CURVE_SAMPLES as f64;
let p = mate.curve.evaluate(t).map_err(|error| format!("{name}: curve: {error}"))?;
samples.push((t, p));
}
let length: f64 = samples.windows(2).map(|w| w[1].1.sub(w[0].1).length()).sum();
if !(length.is_finite() && length > ASM_DIR_EPS) {
return Err(format!("{name}: curve has no length"));
}
let closed = samples[0].1.sub(samples[CURVE_SAMPLES].1).length() <= 1e-9 * length.max(1.0);
Ok(Self { curve: mate.curve.clone(), t0, t1, closed, length, samples })
}
fn closest(&self, p: Vec3) -> (f64, Vec3) {
let (mut t, mut best) = self.samples[0];
let mut best_d = best.sub(p).length();
for &(ts, ps) in &self.samples[1..] {
let d = ps.sub(p).length();
if d < best_d {
(t, best, best_d) = (ts, ps, d);
}
}
let span = self.t1 - self.t0;
for _ in 0..CURVE_NEWTON_STEPS {
let Ok(d) = self.curve.derivatives(t, 2) else { break };
let (c, c1, c2) = (d[0], d[1], d[2]);
let r = c.sub(p);
let g = r.dot(c1);
let h = c1.dot(c1) + r.dot(c2);
if !(h.is_finite() && h > 0.0) {
break;
}
let mut next = t - g / h;
next = if self.closed {
self.t0 + (next - self.t0).rem_euclid(span)
} else {
next.clamp(self.t0, self.t1)
};
if (next - t).abs() <= 1e-15 * span.max(1.0) {
t = next;
break;
}
t = next;
}
let point = self.curve.evaluate(t).unwrap_or(best);
(t, point)
}
pub(super) fn scale(&self) -> f64 {
self.samples
.iter()
.map(|(_, p)| p.x.abs().max(p.y.abs()).max(p.z.abs()))
.fold(self.length, f64::max)
}
pub(super) fn start(&self) -> Vec3 {
self.samples[0].1
}
pub(super) fn orientation(&self, normal: Vec3) -> f64 {
if !self.closed {
return 1.0;
}
let o = self.samples[0].1;
let twice_area: f64 = self
.samples
.windows(2)
.map(|w| w[0].1.sub(o).cross(w[1].1.sub(o)).dot(normal))
.sum();
if twice_area < 0.0 { -1.0 } else { 1.0 }
}
fn fraction(&self, t: f64) -> f64 {
(t - self.t0) / (self.t1 - self.t0)
}
}
#[derive(Clone, Debug)]
pub(super) struct CurveRef {
pub(super) body: usize,
pub(super) curve: Arc<SampledCurve>,
}
fn quat_conjugate(q: [f64; 4]) -> [f64; 4] {
[q[0], -q[1], -q[2], -q[3]]
}
fn closest_world(poses: &[Pose], curve: &CurveRef, wp: Vec3) -> (f64, Vec3) {
let pose = poses[curve.body];
let local = quat_rotate(quat_conjugate(pose.q), wp.sub(pose.t));
let (t, c) = curve.curve.closest(local);
(t, quat_rotate(pose.q, c).add(pose.t))
}
pub(super) fn eval_point_on_curve(poses: &[Pose], p: LocalPoint, curve: &CurveRef, out: &mut Vec<f64>) {
let wp = quat_rotate(poses[p.body].q, p.p).add(poses[p.body].t);
let (_, wc) = closest_world(poses, curve, wp);
let r = wp.sub(wc);
out.extend([r.x, r.y, r.z]);
}
fn axis_crossing(curve: &CurveRef, poses: &[Pose], wp: Vec3, wd: Vec3) -> Vec3 {
let pose = poses[curve.body];
let start = quat_rotate(pose.q, curve.curve.samples[0].1).add(pose.t);
wp.sub(wd.scale(wd.dot(wp.sub(start))))
}
pub(super) fn eval_axis_on_curve(poses: &[Pose], p: LocalPoint, d: LocalDir, curve: &CurveRef, out: &mut Vec<f64>) {
let wp = quat_rotate(poses[p.body].q, p.p).add(poses[p.body].t);
let wd = quat_rotate(poses[d.body].q, d.d);
let (_, wc) = closest_world(poses, curve, axis_crossing(curve, poses, wp, wd));
let v = wp.sub(wc);
let lateral = v.sub(wd.scale(wd.dot(v)));
out.extend([lateral.x, lateral.y, lateral.z]);
}
pub(super) fn eval_curve_along(
poses: &[Pose],
p: LocalPoint,
axis: Option<LocalDir>,
curve: &CurveRef,
target: f64,
reverse: bool,
out: &mut Vec<f64>,
) {
let mut wp = quat_rotate(poses[p.body].q, p.p).add(poses[p.body].t);
if let Some(d) = axis {
wp = axis_crossing(curve, poses, wp, quat_rotate(poses[d.body].q, d.d));
}
let (t, _) = closest_world(poses, curve, wp);
let mut fraction = curve.curve.fraction(t);
if reverse {
fraction = 1.0 - fraction;
}
let mut delta = fraction - target;
if curve.curve.closed {
delta -= delta.round();
}
out.push(delta * curve.curve.length);
}
#[allow(clippy::too_many_arguments)]
pub(super) fn eval_cam_contact(
poses: &[Pose],
p: LocalPoint,
axis: Option<LocalDir>,
curve: &CurveRef,
normal: LocalDir,
radius: f64,
side: f64,
out: &mut Vec<f64>,
) {
let wp = quat_rotate(poses[p.body].q, p.p).add(poses[p.body].t);
let wn = quat_rotate(poses[normal.body].q, normal.d);
let along = axis.map(|d| quat_rotate(poses[d.body].q, d.d)).unwrap_or(wn);
let at = axis_crossing(curve, poses, wp, along);
let (t, wc) = closest_world(poses, curve, at);
let pose = poses[curve.body];
let tangent = curve
.curve
.curve
.derivatives(t, 1)
.ok()
.map(|d| quat_rotate(pose.q, d[1]))
.unwrap_or(Vec3::new(0.0, 0.0, 0.0));
let outward = tangent.cross(wn);
let length = outward.length();
let v = at.sub(wc);
let signed = if length > ASM_DIR_EPS { v.dot(outward.scale(1.0 / length)) } else { v.length() };
out.push(side * signed - radius);
}