blob: 84cf254fb7b9498fee2b37e87055aa91b5dafe63 [file] [log] [blame]
// Copyright 2023 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT OR Unlicense
//! Utility functions for Euler Spiral based stroke expansion.
// Use the same constants as the f64 version.
#![allow(clippy::excessive_precision)]
use super::util::Vec2;
use std::f32::consts::FRAC_PI_4;
// Threshold for tangents to be considered near zero length
pub const TANGENT_THRESH: f32 = 1e-6;
/// This struct contains parameters derived from a cubic Bézier for the
/// purpose of fitting a G1 continuous Euler spiral segment and estimating
/// the Fréchet distance.
///
/// The tangent angles represent deviation from the chord, so that when they
/// are equal, the corresponding Euler spiral is a circular arc.
#[derive(Debug)]
pub struct CubicParams {
/// Tangent angle relative to chord at start.
pub th0: f32,
/// Tangent angle relative to chord at end.
pub th1: f32,
/// The effective chord length, always a robustly nonzero value.
pub chord_len: f32,
/// The estimated error between the source cubic and the proposed Euler spiral.
pub err: f32,
}
#[derive(Debug)]
pub struct EulerParams {
pub th0: f32,
pub th1: f32,
pub k0: f32,
pub k1: f32,
pub ch: f32,
}
#[derive(Debug)]
pub struct EulerSeg {
pub p0: Vec2,
pub p1: Vec2,
pub params: EulerParams,
}
impl CubicParams {
/// Compute parameters from endpoints and derivatives.
///
/// This function is designed to be robust across a wide range of inputs. In
/// particular, it splits between near-zero chord length and the happy path.
/// In the former case, the parameters for the Euler spiral would not be valid,
/// so it proposes a straight line and computes a pretty good (conservative)
/// estimate of the Fréchet distance between that line and the source cubic.
///
/// Computing an accurate estimate here fixes two tricky cases: very short
/// lines, in which the error will be below threshold and the flatten logic will
/// output a single line segment without subdividing, and loop cases with a
/// short chord, in which case the error will exceed the threshold, and the
/// chords of the subdivided pieces will be longer.
///
/// An additional case is the near-cusp where the proposed Euler spiral has
/// a 180 degree U-turn (or, more generally, one angle exceeds 90 degrees and
/// the other does not). In that case, the resulting Euler spiral is quite
/// well defined (with finite curvature, so that its offset will generate a
/// near-semicircle, preserving G1 continuity), but the analytic error
/// calculation would be a huge overestimate. In that case, we just return
/// a rough estimate of the distance between the chord and the spiral segment.
pub fn from_points_derivs(p0: Vec2, p1: Vec2, q0: Vec2, q1: Vec2, dt: f32) -> Self {
let chord = p1 - p0;
let chord_squared = chord.length_squared();
let chord_len = chord_squared.sqrt();
// Chord is near-zero; straight line case.
if chord_squared < TANGENT_THRESH.powi(2) {
// This error estimate was determined empirically through randomized
// testing, though it is likely it can be derived analytically.
let chord_err = ((9. / 32.0) * (q0.length_squared() + q1.length_squared())).sqrt() * dt;
return CubicParams {
th0: 0.0,
th1: 0.0,
chord_len: TANGENT_THRESH,
err: chord_err,
};
}
let scale = dt / chord_squared;
let h0 = Vec2::new(
q0.x * chord.x + q0.y * chord.y,
q0.y * chord.x - q0.x * chord.y,
);
let th0 = h0.atan2();
let d0 = h0.length() * scale;
let h1 = Vec2::new(
q1.x * chord.x + q1.y * chord.y,
q1.x * chord.y - q1.y * chord.x,
);
let th1 = h1.atan2();
let d1 = h1.length() * scale;
// Robustness note: we may want to clamp the magnitude of the angles to
// a bit less than pi. Perhaps here, perhaps downstream.
// Estimate error of geometric Hermite interpolation to Euler spiral.
let cth0 = th0.cos();
let cth1 = th1.cos();
let mut err = if cth0 * cth1 < 0.0 {
// A value of 2.0 represents the approximate worst case distance
// from an Euler spiral with 0 and pi tangents to the chord. It
// is not very critical; doubling the value would result in one more
// subdivision in effectively a binary search for the cusp, while too
// small a value may result in the actual error exceeding the bound.
2.0
} else {
// Protect against divide-by-zero. This happens with a double cusp, so
// should in the general case cause subdivisions.
let e0 = (2. / 3.) / (1.0 + cth0).max(1e-9);
let e1 = (2. / 3.) / (1.0 + cth1).max(1e-9);
let s0 = th0.sin();
let s1 = th1.sin();
// Note: some other versions take sin of s0 + s1 instead. Those are incorrect.
// Strangely, calibration is the same, but more work could be done.
let s01 = cth0 * s1 + cth1 * s0;
let amin = 0.15 * (2. * e0 * s0 + 2. * e1 * s1 - e0 * e1 * s01);
let a = 0.15 * (2. * d0 * s0 + 2. * d1 * s1 - d0 * d1 * s01);
let aerr = (a - amin).abs();
let symm = (th0 + th1).abs();
let asymm = (th0 - th1).abs();
let dist = (d0 - e0).hypot(d1 - e1);
let ctr = 4.625e-6 * symm.powi(5) + 7.5e-3 * asymm * symm.powi(2);
let halo_symm = 5e-3 * symm * dist;
let halo_asymm = 7e-2 * asymm * dist;
/*
println!(" e0: {e0}");
println!(" e1: {e1}");
println!(" s0: {s0}");
println!(" s1: {s1}");
println!(" s01: {s01}");
println!(" amin: {amin}");
println!(" a: {a}");
println!(" aerr: {aerr}");
println!(" symm: {symm}");
println!(" asymm: {asymm}");
println!(" dist: {dist}");
println!(" ctr: {ctr}");
println!(" halo_symm: {halo_symm}");
println!(" halo_asymm: {halo_asymm}");
*/
ctr + 1.55 * aerr + halo_symm + halo_asymm
};
err *= chord_len;
CubicParams {
th0,
th1,
chord_len,
err,
}
}
}
impl EulerParams {
pub fn from_angles(th0: f32, th1: f32) -> EulerParams {
let k0 = th0 + th1;
let dth = th1 - th0;
let d2 = dth * dth;
let k2 = k0 * k0;
let mut a = 6.0;
a -= d2 * (1. / 70.);
a -= (d2 * d2) * (1. / 10780.);
a += (d2 * d2 * d2) * 2.769178184818219e-07;
let b = -0.1 + d2 * (1. / 4200.) + d2 * d2 * 1.6959677820260655e-05;
let c = -1. / 1400. + d2 * 6.84915970574303e-05 - k2 * 7.936475029053326e-06;
a += (b + c * k2) * k2;
let k1 = dth * a;
// calculation of chord
let mut ch = 1.0;
ch -= d2 * (1. / 40.);
ch += (d2 * d2) * 0.00034226190482569864;
ch -= (d2 * d2 * d2) * 1.9349474568904524e-06;
let b = -1. / 24. + d2 * 0.0024702380951963226 - d2 * d2 * 3.7297408997537985e-05;
let c = 1. / 1920. - d2 * 4.87350869747975e-05 - k2 * 3.1001936068463107e-06;
ch += (b + c * k2) * k2;
EulerParams {
th0,
th1,
k0,
k1,
ch,
}
}
pub fn eval_th(&self, t: f32) -> f32 {
(self.k0 + 0.5 * self.k1 * (t - 1.0)) * t - self.th0
}
/// Evaluate the curve at the given parameter.
///
/// The parameter is in the range 0..1, and the result goes from (0, 0) to (1, 0).
fn eval(&self, t: f32) -> Vec2 {
let thm = self.eval_th(t * 0.5);
let k0 = self.k0;
let k1 = self.k1;
let (u, v) = integ_euler_10((k0 + k1 * (0.5 * t - 0.5)) * t, k1 * t * t);
let s = t / self.ch * thm.sin();
let c = t / self.ch * thm.cos();
let x = u * c - v * s;
let y = -v * c - u * s;
Vec2::new(x, y)
}
// Offset provided is in same units as curve; chord is normalized to (1, 0).
fn eval_with_offset(&self, t: f32, offset: f32) -> Vec2 {
let th = self.eval_th(t);
let v = Vec2::new(offset * th.sin(), offset * th.cos());
self.eval(t) + v
}
}
impl EulerSeg {
pub fn from_params(p0: Vec2, p1: Vec2, params: EulerParams) -> Self {
EulerSeg { p0, p1, params }
}
#[allow(unused)]
pub fn eval(&self, t: f32) -> Vec2 {
let Vec2 { x, y } = self.params.eval(t);
let chord = self.p1 - self.p0;
Vec2::new(
self.p0.x + chord.x * x - chord.y * y,
self.p0.y + chord.x * y + chord.y * x,
)
}
// Note: offset provided is normalized so that 1 = chord length, while
// the return value is in the same coordinate space as the endpoints.
pub fn eval_with_offset(&self, t: f32, normalized_offset: f32) -> Vec2 {
let chord = self.p1 - self.p0;
let Vec2 { x, y } = self.params.eval_with_offset(t, normalized_offset);
Vec2::new(
self.p0.x + chord.x * x - chord.y * y,
self.p0.y + chord.x * y + chord.y * x,
)
}
}
/// Integrate Euler spiral.
///
/// This is a 10th order polynomial. The evaluation method is explained in
/// Raph's thesis in section 8.1.2.
///
/// This choice of polynomial is fairly conservative, as it will produce
/// very good accuracy for angles up to about 1 radian, and those angles
/// should almost never happen (the exception being cusps). One possibility
/// to explore is using a lower degree polynomial here, but changing the
/// tuning parameters for subdivision so the additional error here is also
/// factored into the error threshold. However, two cautions against that:
/// First, it doesn't really address the cusp case, where angles will remain
/// large even after further subdivision, and second, the cost of even this
/// more conservative choice is modest; it's just some multiply-adds.
fn integ_euler_10(k0: f32, k1: f32) -> (f32, f32) {
let t1_1 = k0;
let t1_2 = 0.5 * k1;
let t2_2 = t1_1 * t1_1;
let t2_3 = 2. * (t1_1 * t1_2);
let t2_4 = t1_2 * t1_2;
let t3_4 = t2_2 * t1_2 + t2_3 * t1_1;
let t3_6 = t2_4 * t1_2;
let t4_4 = t2_2 * t2_2;
let t4_5 = 2. * (t2_2 * t2_3);
let t4_6 = 2. * (t2_2 * t2_4) + t2_3 * t2_3;
let t4_7 = 2. * (t2_3 * t2_4);
let t4_8 = t2_4 * t2_4;
let t5_6 = t4_4 * t1_2 + t4_5 * t1_1;
let t5_8 = t4_6 * t1_2 + t4_7 * t1_1;
let t6_6 = t4_4 * t2_2;
let t6_7 = t4_4 * t2_3 + t4_5 * t2_2;
let t6_8 = t4_4 * t2_4 + t4_5 * t2_3 + t4_6 * t2_2;
let t7_8 = t6_6 * t1_2 + t6_7 * t1_1;
let t8_8 = t6_6 * t2_2;
let mut u = 1.;
u -= (1. / 24.) * t2_2 + (1. / 160.) * t2_4;
u += (1. / 1920.) * t4_4 + (1. / 10752.) * t4_6 + (1. / 55296.) * t4_8;
u -= (1. / 322560.) * t6_6 + (1. / 1658880.) * t6_8;
u += (1. / 92897280.) * t8_8;
let mut v = (1. / 12.) * t1_2;
v -= (1. / 480.) * t3_4 + (1. / 2688.) * t3_6;
v += (1. / 53760.) * t5_6 + (1. / 276480.) * t5_8;
v -= (1. / 11612160.) * t7_8;
(u, v)
}
const BREAK1: f32 = 0.8;
const BREAK2: f32 = 1.25;
const BREAK3: f32 = 2.1;
const SIN_SCALE: f32 = 1.0976991822760038;
const QUAD_A1: f32 = 0.6406;
const QUAD_B1: f32 = -0.81;
const QUAD_C1: f32 = 0.9148117935952064;
const QUAD_A2: f32 = 0.5;
const QUAD_B2: f32 = -0.156;
const QUAD_C2: f32 = 0.16145779359520596;
pub fn espc_int_approx(x: f32) -> f32 {
let y = x.abs();
let a = if y < BREAK1 {
(SIN_SCALE * y).sin() * (1.0 / SIN_SCALE)
} else if y < BREAK2 {
(8.0f32.sqrt() / 3.0) * (y - 1.0) * (y - 1.0).abs().sqrt() + FRAC_PI_4
} else {
let (a, b, c) = if y < BREAK3 {
(QUAD_A1, QUAD_B1, QUAD_C1)
} else {
(QUAD_A2, QUAD_B2, QUAD_C2)
};
a * y * y + b * y + c
};
a.copysign(x)
}
pub fn espc_int_inv_approx(x: f32) -> f32 {
let y = x.abs();
let a = if y < 0.7010707591262915 {
(x * SIN_SCALE).asin() * (1.0 / SIN_SCALE)
} else if y < 0.903249293595206 {
let b = y - FRAC_PI_4;
let u = b.abs().powf(2. / 3.).copysign(b);
u * (9.0f32 / 8.).cbrt() + 1.0
} else {
let (u, v, w) = if y < 2.038857793595206 {
const B: f32 = 0.5 * QUAD_B1 / QUAD_A1;
(B * B - QUAD_C1 / QUAD_A1, 1.0 / QUAD_A1, B)
} else {
const B: f32 = 0.5 * QUAD_B2 / QUAD_A2;
(B * B - QUAD_C2 / QUAD_A2, 1.0 / QUAD_A2, B)
};
(u + v * y).sqrt() - w
};
a.copysign(x)
}