use serde_json::Value;
use super::doc::{id_key, SketchDoc, SketchPoint};
const SPAN_SAMPLES: usize = 64;
const MIN_SPAN_PARAM: f64 = 1e-3;
pub fn is_spline_type(geom_type: &str) -> bool {
geom_type == "bezier" || geom_type == "spline"
}
#[derive(Clone, Debug)]
pub struct InsertedAnchor {
pub anchor: Value,
pub before: Value,
pub after: Value,
}
pub fn insert_anchor(doc: &mut SketchDoc, u: f64, v: f64, radius: f64) -> Option<InsertedAnchor> {
if doc
.points
.iter()
.any(|p| (p.x - u).hypot(p.y - v) <= radius)
{
return None;
}
let (geo_id, span, t) = nearest_spline_span(doc, u, v, radius)?;
if !(MIN_SPAN_PARAM..=1.0 - MIN_SPAN_PARAM).contains(&t) {
return None;
}
let ids = {
let geo = doc.geometry(&geo_id)?;
let i0 = span * 3;
[
geo.points.get(i0)?.clone(),
geo.points.get(i0 + 1)?.clone(),
geo.points.get(i0 + 2)?.clone(),
geo.points.get(i0 + 3)?.clone(),
]
};
let keys: Vec<String> = ids.iter().map(id_key).collect();
for i in 0..keys.len() {
for j in (i + 1)..keys.len() {
if keys[i] == keys[j] {
return None; }
}
}
let controls = span_controls(doc, &ids)?;
let [p0, p1, p2, p3] = controls;
let a = lerp(p0, p1, t);
let b = lerp(p1, p2, t);
let c = lerp(p2, p3, t);
let d = lerp(a, b, t);
let e = lerp(b, c, t);
let s = lerp(d, e, t);
move_point(doc, &ids[1], a)?;
move_point(doc, &ids[2], c)?;
let d_id = add_point(doc, d);
let s_id = add_point(doc, s);
let e_id = add_point(doc, e);
let geo = doc.geometry_mut(&geo_id)?;
let at = span * 3 + 2;
geo.points
.splice(at..at, [d_id.clone(), s_id.clone(), e_id.clone()]);
Some(InsertedAnchor { anchor: s_id, before: d_id, after: e_id })
}
fn nearest_spline_span(doc: &SketchDoc, u: f64, v: f64, radius: f64) -> Option<(Value, usize, f64)> {
let mut best: Option<(f64, Value, usize, f64)> = None;
for geo in &doc.geometries {
if !is_spline_type(&geo.geom_type) {
continue;
}
let ids = &geo.points;
if ids.len() < 4 || (ids.len() - 1) % 3 != 0 {
continue;
}
for span in 0..(ids.len() - 1) / 3 {
let i0 = span * 3;
let Some(controls) = span_controls(
doc,
&[
ids[i0].clone(),
ids[i0 + 1].clone(),
ids[i0 + 2].clone(),
ids[i0 + 3].clone(),
],
) else {
continue;
};
let (dist, t) = closest_on_span(&controls, u, v);
if dist <= radius && best.as_ref().map_or(true, |(bd, ..)| dist < *bd) {
best = Some((dist, geo.id.clone(), span, t));
}
}
}
best.map(|(_, id, span, t)| (id, span, t))
}
fn closest_on_span(controls: &[[f64; 2]; 4], u: f64, v: f64) -> (f64, f64) {
let mut prev = eval_cubic(controls, 0.0);
let mut best = ((u - prev[0]).hypot(v - prev[1]), 0.0);
for i in 1..=SPAN_SAMPLES {
let t1 = i as f64 / SPAN_SAMPLES as f64;
let next = eval_cubic(controls, t1);
let (dx, dy) = (next[0] - prev[0], next[1] - prev[1]);
let len2 = (dx * dx + dy * dy).max(1e-24);
let s = (((u - prev[0]) * dx + (v - prev[1]) * dy) / len2).clamp(0.0, 1.0);
let dist = (u - (prev[0] + dx * s)).hypot(v - (prev[1] + dy * s));
if dist < best.0 {
let t0 = (i - 1) as f64 / SPAN_SAMPLES as f64;
best = (dist, t0 + (t1 - t0) * s);
}
prev = next;
}
best
}
fn eval_cubic(controls: &[[f64; 2]; 4], t: f64) -> [f64; 2] {
let mt = 1.0 - t;
let (w0, w1, w2, w3) = (
mt * mt * mt,
3.0 * mt * mt * t,
3.0 * mt * t * t,
t * t * t,
);
[
w0 * controls[0][0] + w1 * controls[1][0] + w2 * controls[2][0] + w3 * controls[3][0],
w0 * controls[0][1] + w1 * controls[1][1] + w2 * controls[2][1] + w3 * controls[3][1],
]
}
fn span_controls(doc: &SketchDoc, ids: &[Value; 4]) -> Option<[[f64; 2]; 4]> {
let mut out = [[0.0; 2]; 4];
for (slot, id) in ids.iter().enumerate() {
let p = doc.point(id)?;
out[slot] = [p.x, p.y];
}
Some(out)
}
fn lerp(a: [f64; 2], b: [f64; 2], t: f64) -> [f64; 2] {
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]
}
fn move_point(doc: &mut SketchDoc, id: &Value, at: [f64; 2]) -> Option<()> {
let p = doc.point_mut(id)?;
p.x = at[0];
p.y = at[1];
Some(())
}
fn add_point(doc: &mut SketchDoc, at: [f64; 2]) -> Value {
let id = doc.next_point_id();
doc.points.push(SketchPoint {
id: id.clone(),
x: at[0],
y: at[1],
fixed: false,
construction: false,
external_reference: false,
});
id
}