use serde_json::Value;
use super::doc::{id_key, SketchDoc, SketchGeometry, 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
}
pub fn anchor_handle_pair(doc: &SketchDoc, a: &Value, b: &Value) -> Option<(Value, Value)> {
anchor_handle_side(doc, a, b).map(|side| (side.anchor, side.handle))
}
#[derive(Clone, Debug)]
pub struct GuideSide {
pub geometry: Value,
pub anchor_slot: usize,
pub seg_count: usize,
pub anchor: Value,
pub handle: Value,
}
impl GuideSide {
pub fn interior(&self) -> bool {
self.anchor_slot > 0 && self.anchor_slot < 3 * self.seg_count
}
}
pub fn anchor_handle_side(doc: &SketchDoc, a: &Value, b: &Value) -> Option<GuideSide> {
let (key_a, key_b) = (id_key(a), id_key(b));
for geo in &doc.geometries {
if !is_spline_type(&geo.geom_type) || geo.points.len() < 4 {
continue;
}
let seg_count = (geo.points.len() - 1) / 3;
for i in 0..=seg_count {
let anchor_slot = 3 * i;
let anchor_key = id_key(&geo.points[anchor_slot]);
if anchor_key != key_a && anchor_key != key_b {
continue;
}
let before = (i > 0).then(|| anchor_slot - 1);
let after = (i < seg_count).then_some(anchor_slot + 1);
for slot in [before, after].into_iter().flatten() {
let handle_key = id_key(&geo.points[slot]);
let (anchor, handle) = if anchor_key == key_a && handle_key == key_b {
(a.clone(), b.clone())
} else if anchor_key == key_b && handle_key == key_a {
(b.clone(), a.clone())
} else {
continue;
};
return Some(GuideSide {
geometry: geo.id.clone(),
anchor_slot,
seg_count,
anchor,
handle,
});
}
}
}
None
}
pub fn guide_side(doc: &SketchDoc, geo: &SketchGeometry) -> Option<GuideSide> {
if geo.geom_type != "line" || geo.points.len() < 2 {
return None;
}
anchor_handle_side(doc, &geo.points[0], &geo.points[1])
}
pub fn is_curvature_joint(doc: &SketchDoc, a: &SketchGeometry, b: &SketchGeometry) -> bool {
let (Some(side_a), Some(side_b)) = (guide_side(doc, a), guide_side(doc, b)) else {
return false;
};
if id_key(&side_a.handle) == id_key(&side_b.handle) {
return false;
}
let same_anchor = id_key(&side_a.anchor) == id_key(&side_b.anchor);
if id_key(&side_a.geometry) == id_key(&side_b.geometry)
&& side_a.anchor_slot == side_b.anchor_slot
&& side_a.interior()
&& same_anchor
{
return true;
}
if side_a.interior() || side_b.interior() {
return false;
}
same_anchor || anchors_coincident(doc, &side_a.anchor, &side_b.anchor)
}
fn anchors_coincident(doc: &SketchDoc, a: &Value, b: &Value) -> bool {
let (key_a, key_b) = (id_key(a), id_key(b));
doc.constraints.iter().any(|c| {
if c.ctype() != Some("≡") {
return false;
}
let keys: Vec<String> = c.points().iter().map(id_key).collect();
keys.contains(&key_a) && keys.contains(&key_b)
})
}
pub fn is_handle_guide(doc: &SketchDoc, geo: &SketchGeometry) -> bool {
guide_side(doc, geo).is_some()
}
pub fn is_end_handle_guide(doc: &SketchDoc, geo: &SketchGeometry) -> bool {
guide_side(doc, geo).is_some_and(|side| !side.interior())
}