灵符lingfu

ling-lang

↓ 0 downloads

Ling - The Omniglot Systems Language

Keywords systems, polyglot, ai, crypto, game
lingfu install ling-lang
LicenseApache-2.0 OR MIT
AuthorsLing Lin, Sanny Lin
READMECode / FilesVersionsDependency GraphDependentsSecurityBuildsSettings

Code / Files (995)

crates/ling-body/src/curves.rs
//! Crease curves: iso-contours of a scalar field on a triangle mesh, chained into
//! polylines and fitted with cubic Béziers (Schneider's least-squares fit with
//! Newton-Raphson reparameterisation and recursive splitting at the worst point).

use crate::mesh::Mesh;
use std::collections::HashMap;
use std::fmt::Write as _;

pub type Bez = [[f32; 3]; 4];

#[derive(Clone, Debug)]
pub struct Curve {
    pub segs: Vec<Bez>,
    /// Field value the contour follows.
    pub level: f32,
    /// 0.5 for the weakest contour level up to 1.0 for the strongest; scales crease depth.
    pub weight: f32,
    pub closed: bool,
    /// Length of the polyline the curve was fitted to, in metres.
    pub length: f32,
}

type V = [f64; 3];

fn sub(a: V, b: V) -> V {
    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn add(a: V, b: V) -> V {
    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn mul(a: V, s: f64) -> V {
    [a[0] * s, a[1] * s, a[2] * s]
}
fn dot(a: V, b: V) -> f64 {
    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn norm(a: V) -> f64 {
    dot(a, a).sqrt()
}
fn unit(a: V) -> V {
    let n = norm(a);
    if n < 1e-12 {
        [0.0, 0.0, 0.0]
    } else {
        mul(a, 1.0 / n)
    }
}

/// Contour vertices (one per crossed mesh edge) and their links.
#[derive(Default)]
struct Chain {
    pts: Vec<V>,
    adj: Vec<Vec<usize>>,
    index: HashMap<(u32, u32), usize>,
}

impl Chain {
    fn crossing(&mut self, mesh: &Mesh, s: &[f32], level: f32, a: u32, b: u32) -> usize {
        let key = (a.min(b), a.max(b));
        if let Some(&i) = self.index.get(&key) {
            return i;
        }
        let (sa, sb) = (s[a as usize], s[b as usize]);
        let t = ((level - sa) / (sb - sa)) as f64;
        let p = |i: u32| {
            let o = i as usize * 3;
            [mesh.pos[o] as f64, mesh.pos[o + 1] as f64, mesh.pos[o + 2] as f64]
        };
        let (pa, pb) = (p(a), p(b));
        self.pts.push(add(pa, mul(sub(pb, pa), t)));
        self.adj.push(Vec::new());
        self.index.insert(key, self.pts.len() - 1);
        self.pts.len() - 1
    }
}

/// Polylines (with a closed flag) where `s` crosses `level` on the mesh surface.
fn contour_lines(mesh: &Mesh, s: &[f32], level: f32) -> Vec<(Vec<V>, bool)> {
    let mut chain = Chain::default();
    for t in mesh.idx.as_chunks::<3>().0 {
        let above = [s[t[0] as usize] >= level, s[t[1] as usize] >= level, s[t[2] as usize] >= level];
        let n = above.iter().filter(|&&b| b).count();
        if n == 0 || n == 3 {
            continue;
        }
        let odd = (0..3).find(|&k| above[k] == (n == 1)).unwrap_or(0);
        let (o1, o2) = (t[(odd + 1) % 3], t[(odd + 2) % 3]);
        let p = chain.crossing(mesh, s, level, t[odd], o1);
        let q = chain.crossing(mesh, s, level, t[odd], o2);
        chain.adj[p].push(q);
        chain.adj[q].push(p);
    }
    let Chain { pts, adj, .. } = chain;

    let mut seen = vec![false; pts.len()];
    let mut lines = Vec::new();
    let walk = |start: usize, seen: &mut Vec<bool>| {
        let mut path = vec![start];
        seen[start] = true;
        let mut cur = start;
        while let Some(&next) = adj[cur].iter().find(|&&n| !seen[n]) {
            seen[next] = true;
            path.push(next);
            cur = next;
        }
        let closed = path.len() > 2 && adj[cur].contains(&start);
        (path, closed)
    };
    for start in 0..pts.len() {
        if !seen[start] && adj[start].len() == 1 {
            let (path, closed) = walk(start, &mut seen);
            lines.push((path.iter().map(|&i| pts[i]).collect(), closed));
        }
    }
    for start in 0..pts.len() {
        if !seen[start] {
            let (path, closed) = walk(start, &mut seen);
            lines.push((path.iter().map(|&i| pts[i]).collect(), closed));
        }
    }
    lines
}

fn smooth_line(p: &mut [V], closed: bool, passes: usize) {
    let n = p.len();
    if n < 3 {
        return;
    }
    for _ in 0..passes {
        let src = p.to_vec();
        for i in 0..n {
            let (prev, next) = if closed {
                (src[(i + n - 1) % n], src[(i + 1) % n])
            } else if i == 0 || i == n - 1 {
                continue;
            } else {
                (src[i - 1], src[i + 1])
            };
            p[i] = add(mul(src[i], 0.5), mul(add(prev, next), 0.25));
        }
    }
}

fn line_length(p: &[V]) -> f64 {
    p.windows(2).map(|w| norm(sub(w[1], w[0]))).sum()
}

fn bez_point(b: &[V; 4], t: f64) -> V {
    let m = 1.0 - t;
    let (c0, c1, c2, c3) = (m * m * m, 3.0 * m * m * t, 3.0 * m * t * t, t * t * t);
    [0, 1, 2].map(|k| b[0][k] * c0 + b[1][k] * c1 + b[2][k] * c2 + b[3][k] * c3)
}

fn bez_d1(b: &[V; 4], t: f64) -> V {
    let m = 1.0 - t;
    let q = [sub(b[1], b[0]), sub(b[2], b[1]), sub(b[3], b[2])];
    add(add(mul(q[0], 3.0 * m * m), mul(q[1], 6.0 * m * t)), mul(q[2], 3.0 * t * t))
}

fn bez_d2(b: &[V; 4], t: f64) -> V {
    let m = 1.0 - t;
    let q = [
        add(sub(b[2], mul(b[1], 2.0)), b[0]),
        add(sub(b[3], mul(b[2], 2.0)), b[1]),
    ];
    add(mul(q[0], 6.0 * m), mul(q[1], 6.0 * t))
}

fn chord_params(d: &[V]) -> Vec<f64> {
    let mut u = vec![0.0];
    for w in d.windows(2) {
        u.push(u.last().copied().unwrap_or(0.0) + norm(sub(w[1], w[0])));
    }
    let total = u.last().copied().unwrap_or(1.0).max(1e-12);
    u.iter().map(|v| v / total).collect()
}

fn generate(d: &[V], u: &[f64], t1: V, t2: V) -> [V; 4] {
    let (first, last) = (d[0], d[d.len() - 1]);
    let (mut c00, mut c01, mut c11, mut x0, mut x1) = (0.0, 0.0, 0.0, 0.0, 0.0);
    for (p, &t) in d.iter().zip(u) {
        let m = 1.0 - t;
        let (b0, b1, b2, b3) = (m * m * m, 3.0 * t * m * m, 3.0 * t * t * m, t * t * t);
        let (a0, a1) = (mul(t1, b1), mul(t2, b2));
        c00 += dot(a0, a0);
        c01 += dot(a0, a1);
        c11 += dot(a1, a1);
        let tmp = sub(*p, add(mul(first, b0 + b1), mul(last, b2 + b3)));
        x0 += dot(a0, tmp);
        x1 += dot(a1, tmp);
    }
    let det = c00 * c11 - c01 * c01;
    let (al, ar) = if det.abs() > 1e-12 { ((x0 * c11 - x1 * c01) / det, (c00 * x1 - c01 * x0) / det) } else { (0.0, 0.0) };
    let seg = norm(sub(last, first));
    let eps = 1e-6 * seg;
    if al < eps || ar < eps {
        let third = seg / 3.0;
        [first, add(first, mul(t1, third)), add(last, mul(t2, third)), last]
    } else {
        [first, add(first, mul(t1, al)), add(last, mul(t2, ar)), last]
    }
}

fn reparameterize(d: &[V], u: &[f64], b: &[V; 4]) -> Vec<f64> {
    d.iter()
        .zip(u)
        .map(|(&p, &t)| {
            let q = sub(bez_point(b, t), p);
            let (d1, d2) = (bez_d1(b, t), bez_d2(b, t));
            let num = dot(q, d1);
            let den = dot(d1, d1) + dot(q, d2);
            if den.abs() < 1e-12 {
                t
            } else {
                (t - num / den).clamp(0.0, 1.0)
            }
        })
        .collect()
}

fn max_error(d: &[V], u: &[f64], b: &[V; 4]) -> (f64, usize) {
    let mut worst = (0.0, d.len() / 2);
    for i in 1..d.len() - 1 {
        let e = sub(bez_point(b, u[i]), d[i]);
        let e2 = dot(e, e);
        if e2 > worst.0 {
            worst = (e2, i);
        }
    }
    worst
}

fn fit_cubic(d: &[V], t1: V, t2: V, err2: f64, out: &mut Vec<[V; 4]>) {
    if d.len() == 2 {
        let third = norm(sub(d[1], d[0])) / 3.0;
        out.push([d[0], add(d[0], mul(t1, third)), add(d[1], mul(t2, third)), d[1]]);
        return;
    }
    let mut u = chord_params(d);
    let mut b = generate(d, &u, t1, t2);
    let (mut worst, mut split) = max_error(d, &u, &b);
    if worst < err2 {
        out.push(b);
        return;
    }
    if worst < err2 * 16.0 {
        for _ in 0..6 {
            u = reparameterize(d, &u, &b);
            b = generate(d, &u, t1, t2);
            (worst, split) = max_error(d, &u, &b);
            if worst < err2 {
                out.push(b);
                return;
            }
        }
    }
    let split = split.clamp(1, d.len() - 2);
    let center = unit(sub(d[split - 1], d[split + 1]));
    fit_cubic(&d[..=split], t1, center, err2, out);
    fit_cubic(&d[split..], mul(center, -1.0), t2, err2, out);
}

fn fit_line(p: &[V], tol: f64) -> Vec<[V; 4]> {
    let mut out = Vec::new();
    if p.len() < 2 {
        return out;
    }
    let t1 = unit(sub(p[1], p[0]));
    let t2 = unit(sub(p[p.len() - 2], p[p.len() - 1]));
    fit_cubic(p, t1, t2, tol * tol, &mut out);
    out
}

/// Settings for turning a scalar field into fitted crease curves.
pub struct CreaseCfg {
    /// Number of contour levels, spread over the upper quantiles of the field.
    pub levels: usize,
    /// Fitting tolerance in metres.
    pub tolerance: f32,
    /// Curves with a shorter polyline are dropped.
    pub min_length: f32,
}

/// Iso-contours of `s` on `mesh` at `levels` quantile-spaced values, or at zero when
/// `nodal` is set (pressure nodal lines), fitted as cubic Bézier chains.
pub fn detect(mesh: &Mesh, s: &[f32], cfg: &CreaseCfg, nodal: bool) -> Vec<Curve> {
    let mut sorted: Vec<f32> = s.to_vec();
    sorted.sort_by(|a, b| a.total_cmp(b));
    if sorted.is_empty() {
        return Vec::new();
    }
    let levels: Vec<(f32, f32)> = if nodal {
        vec![(0.0, 1.0)]
    } else {
        let n = cfg.levels.max(1);
        (0..n)
            .map(|k| {
                let f = if n == 1 { 0.0 } else { k as f32 / (n - 1) as f32 };
                let q = 0.55 + 0.4 * f;
                (sorted[((sorted.len() - 1) as f32 * q) as usize], 0.5 + 0.5 * f)
            })
            .collect()
    };
    let mut curves = Vec::new();
    for (level, weight) in levels {
        for (mut line, closed) in contour_lines(mesh, s, level) {
            smooth_line(&mut line, closed, 3);
            if closed {
                line.push(line[0]);
            }
            line.dedup_by(|a, b| norm(sub(*a, *b)) < 1e-6);
            let length = line_length(&line);
            if line.len() < 3 || length < cfg.min_length as f64 {
                continue;
            }
            let segs: Vec<Bez> = fit_line(&line, cfg.tolerance as f64)
                .into_iter()
                .map(|b| b.map(|p| [p[0] as f32, p[1] as f32, p[2] as f32]))
                .collect();
            curves.push(Curve { segs, level, weight, closed, length: length as f32 });
        }
    }
    curves
}

pub fn bez_at(b: &Bez, t: f32) -> [f32; 3] {
    let v = b.map(|p| [p[0] as f64, p[1] as f64, p[2] as f64]);
    let p = bez_point(&v, t as f64);
    [p[0] as f32, p[1] as f32, p[2] as f32]
}

/// Points along a curve, `per_seg` samples per Bézier segment plus the end point.
pub fn flatten(c: &Curve, per_seg: usize) -> Vec<[f32; 3]> {
    let n = per_seg.max(1);
    let mut out = Vec::with_capacity(c.segs.len() * n + 1);
    for seg in &c.segs {
        for k in 0..n {
            out.push(bez_at(seg, k as f32 / n as f32));
        }
    }
    if let Some(last) = c.segs.last() {
        out.push(last[3]);
    }
    out
}

pub fn to_json(curves: &[Curve]) -> String {
    let list: Vec<serde_json::Value> = curves
        .iter()
        .map(|c| {
            serde_json::json!({
                "level": c.level, "weight": c.weight, "closed": c.closed, "length": c.length,
                "segments": c.segs,
            })
        })
        .collect();
    serde_json::to_string_pretty(&serde_json::json!({
        "format": "ling-body/creases", "units": "m", "up": "y", "forward": "z", "curves": list,
    }))
    .unwrap_or_default()
}

/// A Blender script that recreates every crease as a 3D Bézier curve object.
pub fn to_blender_script(curves: &[Curve]) -> String {
    let conv = |p: [f32; 3]| format!("({:.5}, {:.5}, {:.5})", p[0], -p[2], p[1]);
    let mut s = String::from(
        "import bpy\n\n# Creases from ling-body. Y-up metres converted to Blender Z-up.\n\
         def crease(name, anchors, closed):\n\
         \x20   cu = bpy.data.curves.new(name, 'CURVE')\n\
         \x20   cu.dimensions = '3D'\n\
         \x20   cu.bevel_depth = 0.001\n\
         \x20   sp = cu.splines.new('BEZIER')\n\
         \x20   sp.bezier_points.add(len(anchors) - 1)\n\
         \x20   for bp, (co, hl, hr) in zip(sp.bezier_points, anchors):\n\
         \x20       bp.handle_left_type = bp.handle_right_type = 'FREE'\n\
         \x20       bp.co, bp.handle_left, bp.handle_right = co, hl, hr\n\
         \x20   sp.use_cyclic_u = closed\n\
         \x20   ob = bpy.data.objects.new(name, cu)\n\
         \x20   bpy.context.collection.objects.link(ob)\n\n",
    );
    for (i, c) in curves.iter().enumerate() {
        let n = c.segs.len();
        if n == 0 {
            continue;
        }
        let mut anchors: Vec<([f32; 3], [f32; 3], [f32; 3])> = Vec::new();
        for k in 0..=n {
            let (co, left, right) = if k == 0 {
                (c.segs[0][0], c.segs[0][0], c.segs[0][1])
            } else if k == n {
                (c.segs[n - 1][3], c.segs[n - 1][2], c.segs[n - 1][3])
            } else {
                (c.segs[k - 1][3], c.segs[k - 1][2], c.segs[k][1])
            };
            anchors.push((co, left, right));
        }
        if c.closed && anchors.len() > 2 {
            let last = anchors.pop().unwrap_or(anchors[0]);
            anchors[0].1 = last.1;
        }
        let _ = write!(s, "crease('crease_{i}', [");
        for (co, l, r) in &anchors {
            let _ = write!(s, "({}, {}, {}), ", conv(*co), conv(*l), conv(*r));
        }
        let _ = writeln!(s, "], {})", if c.closed { "True" } else { "False" });
    }
    s
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::mesher;
    use crate::Grid;

    fn sphere_mesh() -> Mesh {
        let g = Grid { nx: 40, ny: 40, nz: 40, origin: [-0.4; 3], dx: 0.02 };
        let phi: Vec<f32> = (0..g.len())
            .map(|i| {
                let (x, y, z) = g.coords(i);
                let p = g.world(x, y, z);
                (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt() - 0.25
            })
            .collect();
        mesher::build(&phi, &g, 0.0, 1, 0.0)
    }

    #[test]
    fn latitude_contour_is_one_closed_curve_fitted_within_tolerance() {
        let m = sphere_mesh();
        let s: Vec<f32> = m.pos.as_chunks::<3>().0.iter().map(|p| p[1]).collect();
        let cfg = CreaseCfg { levels: 1, tolerance: 0.002, min_length: 0.1 };
        let level = 0.0;
        let lines = contour_lines(&m, &s, level);
        assert_eq!(lines.len(), 1);
        assert!(lines[0].1, "equator must be closed");
        let curves = detect(&m, &s, &cfg, true);
        assert_eq!(curves.len(), 1);
        let c = &curves[0];
        assert!(c.closed);
        assert!((c.length - 2.0 * std::f32::consts::PI * 0.25).abs() < 0.03, "length {}", c.length);
        for p in flatten(c, 12) {
            let r = (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt();
            assert!((r - 0.25).abs() < 0.01 && p[1].abs() < 0.01, "off the equator: {p:?}");
        }
    }

    #[test]
    fn fewer_segments_than_samples_for_smooth_lines() {
        let line: Vec<V> = (0..200)
            .map(|i| {
                let a = i as f64 / 199.0 * std::f64::consts::PI;
                [a.cos() * 0.3, a.sin() * 0.3, 0.0]
            })
            .collect();
        let segs = fit_line(&line, 0.001);
        assert!(segs.len() <= 6, "{} segments", segs.len());
        let mid = bez_point(&segs[segs.len() / 2], 0.5);
        assert!((norm(mid) - 0.3).abs() < 0.002);
    }

    #[test]
    fn exports_name_every_curve() {
        let m = sphere_mesh();
        let s: Vec<f32> = m.pos.as_chunks::<3>().0.iter().map(|p| p[1]).collect();
        let curves = detect(&m, &s, &CreaseCfg { levels: 2, tolerance: 0.003, min_length: 0.1 }, false);
        assert!(!curves.is_empty());
        let json: serde_json::Value = serde_json::from_str(&to_json(&curves)).unwrap();
        assert_eq!(json["curves"].as_array().unwrap().len(), curves.len());
        let py = to_blender_script(&curves);
        assert_eq!(py.matches("crease('crease_").count(), curves.len());
    }
}

← Back to index