klyff_msdf 0.1.3

MSDF generation library with optional GPU acceleration.
Documentation
//! Math utilities.
//!
//! Based on chapter 2 of chlumsky's thesis.

use crate::math_segment::distance_point_to_line;
use glam::Vec2;

// Sin 175deg
const CORNER_THRESHOLD: f32 = 0.0871557;

pub(crate) fn is_corner(v1: Vec2, v2: Vec2) -> bool {
    v1.dot(v2) <= 0. || cross(v1.normalize(), v2.normalize()).abs() > CORNER_THRESHOLD
}

pub(crate) fn are_very_close_points(a: Vec2, b: Vec2) -> bool {
    a.distance_squared(b) <= 1e-6
}

/// Flattening a quadratic bezier by recursively subdividing with de casteljau.
pub fn flatten_quad(
    p: [Vec2; 3],
    split_at_deviation: f32,
    to_seg: &mut impl FnMut(Vec2, Vec2, Vec2),
) {
    let [p0, p1, p2] = p;
    let p01 = p0.midpoint(p1);
    let p12 = p1.midpoint(p2);
    let mid = p01.midpoint(p12);
    if !flat_enough_quad(p0, p01, mid, split_at_deviation) {
        flatten_quad([p0, p01, mid], split_at_deviation, to_seg);
    } else {
        to_seg(p0, p01, mid);
    }
    if !flat_enough_quad(mid, p12, p2, split_at_deviation) {
        flatten_quad([mid, p12, p2], split_at_deviation, to_seg);
    } else {
        to_seg(mid, p12, p2);
    }
}

/// Flattening a cubic bezier by recursively subdividing with de casteljau.
pub fn flatten_cube(
    p: [Vec2; 4],
    split_at_deviation: f32,
    to_seg: &mut impl FnMut(Vec2, Vec2, Vec2),
) {
    let [p0, p1, p2, p3] = p;
    if flat_enough_cube(p0, p1, p2, p3, split_at_deviation) {
        let (q0, q1, q2) = approx_cubic_as_quad(p0, p1, p2, p3);
        to_seg(q0, q1, q2);
        return;
    }
    let (p01, p012, mid, p123, p23) = split_cube(p0, p1, p2, p3);
    flatten_cube([p0, p01, p012, mid], split_at_deviation, to_seg);
    flatten_cube([mid, p123, p23, p3], split_at_deviation, to_seg);
}

fn approx_cubic_as_quad(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2) -> (Vec2, Vec2, Vec2) {
    let q0 = p0;
    let q2 = p3;
    let q1 = (-p0 + 3. * p1 + 3. * p2 - p3) / 4.;
    (q0, q1, q2)
}

fn flat_enough_quad(p0: Vec2, p1: Vec2, p2: Vec2, split_at_deviation: f32) -> bool {
    let d = distance_point_to_line(p1, p0, p2);
    d <= split_at_deviation
}

fn flat_enough_cube(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2, split_at_deviation: f32) -> bool {
    let q1 = (-p0 + 3.0 * p1 + 3.0 * p2 - p3) * 0.25;
    let d_q1 = distance_point_to_line(q1, p0, p3);
    let d_p1 = distance_point_to_line(p1, p0, p3);
    let d_p2 = distance_point_to_line(p2, p0, p3);
    let delta3 = p3 - 3.0 * p2 + 3.0 * p1 - p0;
    let approx_error = delta3.length() * (1.0 / 20.8);
    // Empirically calibrated: a 16× tighter chord bound matches the segment
    // count produced by the source glyph's quadratic outline of the same
    // character.
    let chord_threshold = split_at_deviation * 0.0625;
    d_q1.max(d_p1).max(d_p2) <= chord_threshold && approx_error <= split_at_deviation
}

pub(crate) fn split_cube(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2) -> (Vec2, Vec2, Vec2, Vec2, Vec2) {
    let p01 = p0.midpoint(p1);
    let p12 = p1.midpoint(p2);
    let p23 = p2.midpoint(p3);
    let p012 = p01.midpoint(p12);
    let p123 = p12.midpoint(p23);
    let mid = p012.midpoint(p123);
    (p01, p012, mid, p123, p23)
}

pub(crate) fn cross(v1: Vec2, v2: Vec2) -> f32 {
    v1.x * v2.y - v1.y * v2.x
}