use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use super::doc::{id_key, SketchConstraint, SketchDoc, SketchGeometry, SketchPoint};
use super::PlaneFrame;
#[derive(Clone, Debug, PartialEq)]
pub enum EdgeLink {
Line { a: (f64, f64), b: (f64, f64) },
Circle { center: (f64, f64), rim: (f64, f64) },
Arc {
center: (f64, f64),
start: (f64, f64),
end: (f64, f64),
},
Polyline { pts: Vec<(f64, f64)> },
}
impl EdgeLink {
pub fn kind(&self) -> &'static str {
match self {
EdgeLink::Line { .. } => "line",
EdgeLink::Circle { .. } => "circle",
EdgeLink::Arc { .. } => "arc",
EdgeLink::Polyline { .. } => "polyline",
}
}
pub fn point_uvs(&self) -> Vec<(f64, f64)> {
match self {
EdgeLink::Line { a, b } => vec![*a, *b],
EdgeLink::Circle { center, rim } => vec![*center, *rim],
EdgeLink::Arc { center, start, end } => vec![*center, *start, *end],
EdgeLink::Polyline { pts } => pts.clone(),
}
}
}
pub fn project_polyline(plane: &PlaneFrame, world: &[[f64; 3]]) -> Vec<(f64, f64)> {
world.iter().map(|&w| plane.to_uv(w)).collect()
}
pub fn classify_uv(uv: &[(f64, f64)]) -> EdgeLink {
let n = uv.len();
if n < 2 {
return EdgeLink::Polyline { pts: uv.to_vec() };
}
let a = uv[0];
let b = uv[n - 1];
let extent = polyline_extent(uv).max(1e-9);
let straight_tol = 1e-4 * extent;
let closed = dist(a, b) <= straight_tol;
if !closed {
let max_dev = uv[1..n - 1]
.iter()
.map(|&p| point_segment_distance(p, a, b))
.fold(0.0_f64, f64::max);
if n == 2 || max_dev <= straight_tol {
return EdgeLink::Line { a, b };
}
}
if n >= 3 {
if let Some((cx, cy, r)) = fit_circle(uv[0], uv[n / 3], uv[(2 * n) / 3]) {
let circle_tol = 1e-3 * extent;
let residual = uv
.iter()
.map(|&p| (dist(p, (cx, cy)) - r).abs())
.fold(0.0_f64, f64::max);
if r.is_finite() && r > straight_tol && residual <= circle_tol {
if closed {
return EdgeLink::Circle {
center: (cx, cy),
rim: (cx + r, cy),
};
}
return EdgeLink::Arc {
center: (cx, cy),
start: a,
end: b,
};
}
}
}
EdgeLink::Polyline { pts: uv.to_vec() }
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ExternalRef {
#[serde(rename = "edgeName")]
pub edge_name: String,
#[serde(rename = "solidName", default)]
pub solid_name: String,
#[serde(rename = "pointIds", default)]
pub point_ids: Vec<Value>,
#[serde(rename = "geomIds", default)]
pub geom_ids: Vec<Value>,
#[serde(default)]
pub kind: String,
}
pub fn link_or_update(
doc: &mut SketchDoc,
refs: &mut Vec<ExternalRef>,
edge_name: &str,
solid_name: &str,
world_poly: &[[f64; 3]],
plane: &PlaneFrame,
) -> bool {
if world_poly.len() < 2 {
return false;
}
let uv = project_polyline(plane, world_poly);
let link = classify_uv(&uv);
let new_uvs = link.point_uvs();
if let Some(pos) = refs.iter().position(|r| r.edge_name == edge_name) {
let structure_matches =
refs[pos].kind == link.kind() && refs[pos].point_ids.len() == new_uvs.len();
if structure_matches {
let mut moved = false;
let point_ids = refs[pos].point_ids.clone();
for (id, (u, v)) in point_ids.iter().zip(new_uvs.iter()) {
if let Some(p) = doc.point_mut(id) {
if (p.x - u).abs() > 1e-12 || (p.y - v).abs() > 1e-12 {
moved = true;
}
p.x = *u;
p.y = *v;
p.fixed = true;
p.construction = true;
p.external_reference = true;
}
}
if refs[pos].solid_name != solid_name {
refs[pos].solid_name = solid_name.to_string();
}
return moved;
}
remove_ref_entities(doc, &refs[pos].clone());
let (point_ids, geom_ids) = add_external_ref(doc, &link);
refs[pos] = ExternalRef {
edge_name: edge_name.to_string(),
solid_name: solid_name.to_string(),
point_ids,
geom_ids,
kind: link.kind().to_string(),
};
return true;
}
let (point_ids, geom_ids) = add_external_ref(doc, &link);
refs.push(ExternalRef {
edge_name: edge_name.to_string(),
solid_name: solid_name.to_string(),
point_ids,
geom_ids,
kind: link.kind().to_string(),
});
true
}
pub fn add_external_ref(doc: &mut SketchDoc, link: &EdgeLink) -> (Vec<Value>, Vec<Value>) {
let mut point_ids = Vec::new();
for (u, v) in link.point_uvs() {
let id = doc.next_point_id();
doc.points.push(SketchPoint {
id: id.clone(),
x: u,
y: v,
fixed: true,
construction: true,
external_reference: true,
});
push_ground(doc, &id);
point_ids.push(id);
}
let geom_ids = match link {
EdgeLink::Line { .. } => vec![push_construction_geometry(
doc,
"line",
vec![point_ids[0].clone(), point_ids[1].clone()],
)],
EdgeLink::Circle { .. } => vec![push_construction_geometry(
doc,
"circle",
vec![point_ids[0].clone(), point_ids[1].clone()],
)],
EdgeLink::Arc { .. } => vec![push_construction_geometry(
doc,
"arc",
vec![
point_ids[0].clone(),
point_ids[1].clone(),
point_ids[2].clone(),
],
)],
EdgeLink::Polyline { .. } => point_ids
.windows(2)
.map(|w| push_construction_geometry(doc, "line", vec![w[0].clone(), w[1].clone()]))
.collect(),
};
(point_ids, geom_ids)
}
fn remove_ref_entities(doc: &mut SketchDoc, r: &ExternalRef) {
let pt_keys: HashSet<String> = r.point_ids.iter().map(id_key).collect();
let geo_keys: HashSet<String> = r.geom_ids.iter().map(id_key).collect();
doc.geometries.retain(|g| !geo_keys.contains(&id_key(&g.id)));
doc.points.retain(|p| !pt_keys.contains(&id_key(&p.id)));
doc.constraints
.retain(|c| !c.points().iter().any(|p| pt_keys.contains(&id_key(p))));
}
fn push_ground(doc: &mut SketchDoc, pid: &Value) {
let cid = doc.next_constraint_id();
let mut raw = Map::new();
raw.insert("id".to_string(), cid);
raw.insert("type".to_string(), Value::String("⏚".to_string()));
raw.insert("points".to_string(), Value::Array(vec![pid.clone()]));
doc.constraints.push(SketchConstraint { raw });
}
fn push_construction_geometry(doc: &mut SketchDoc, geom_type: &str, points: Vec<Value>) -> Value {
let id = doc.next_geometry_id();
let mut extra = Map::new();
extra.insert("construction".to_string(), Value::Bool(true));
doc.geometries.push(SketchGeometry {
id: id.clone(),
geom_type: geom_type.to_string(),
points,
extra,
});
id
}
fn dist(a: (f64, f64), b: (f64, f64)) -> f64 {
((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt()
}
fn polyline_extent(uv: &[(f64, f64)]) -> f64 {
let (mut minx, mut miny) = (f64::INFINITY, f64::INFINITY);
let (mut maxx, mut maxy) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
for &(x, y) in uv {
minx = minx.min(x);
miny = miny.min(y);
maxx = maxx.max(x);
maxy = maxy.max(y);
}
((maxx - minx).powi(2) + (maxy - miny).powi(2)).sqrt()
}
fn point_segment_distance(p: (f64, f64), a: (f64, f64), b: (f64, f64)) -> f64 {
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let len2 = dx * dx + dy * dy;
let t = if len2 <= 1e-18 {
0.0
} else {
(((p.0 - a.0) * dx + (p.1 - a.1) * dy) / len2).clamp(0.0, 1.0)
};
dist(p, (a.0 + t * dx, a.1 + t * dy))
}
fn fit_circle(p1: (f64, f64), p2: (f64, f64), p3: (f64, f64)) -> Option<(f64, f64, f64)> {
let (ax, ay) = p1;
let (bx, by) = p2;
let (cx, cy) = p3;
let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
if d.abs() < 1e-12 {
return None;
}
let a2 = ax * ax + ay * ay;
let b2 = bx * bx + by * by;
let c2 = cx * cx + cy * cy;
let ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d;
let uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d;
let r = dist((ux, uy), p1);
if !ux.is_finite() || !uy.is_finite() || !r.is_finite() {
return None;
}
Some((ux, uy, r))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn empty_doc() -> SketchDoc {
SketchDoc::default()
}
#[test]
fn classify_two_point_polyline_is_a_line() {
let link = classify_uv(&[(0.0, 0.0), (10.0, 5.0)]);
assert_eq!(link, EdgeLink::Line { a: (0.0, 0.0), b: (10.0, 5.0) });
}
#[test]
fn classify_collinear_samples_is_a_line() {
let uv: Vec<(f64, f64)> = (0..=10).map(|i| (i as f64, 2.0 * i as f64)).collect();
assert_eq!(classify_uv(&uv).kind(), "line");
}
#[test]
fn classify_closed_circle() {
let (cx, cy, r) = (3.0, -1.0, 5.0);
let n = 64;
let uv: Vec<(f64, f64)> = (0..=n)
.map(|i| {
let t = i as f64 / n as f64 * std::f64::consts::TAU;
(cx + r * t.cos(), cy + r * t.sin())
})
.collect();
match classify_uv(&uv) {
EdgeLink::Circle { center, rim } => {
assert!((center.0 - cx).abs() < 1e-6 && (center.1 - cy).abs() < 1e-6);
assert!((dist(center, rim) - r).abs() < 1e-6);
}
other => panic!("expected circle, got {other:?}"),
}
}
#[test]
fn classify_open_arc() {
let (cx, cy, r) = (0.0, 0.0, 4.0);
let n = 16;
let uv: Vec<(f64, f64)> = (0..=n)
.map(|i| {
let t = i as f64 / n as f64 * (std::f64::consts::PI / 2.0);
(cx + r * t.cos(), cy + r * t.sin())
})
.collect();
match classify_uv(&uv) {
EdgeLink::Arc { center, start, end } => {
assert!((center.0 - cx).abs() < 1e-6 && (center.1 - cy).abs() < 1e-6);
assert!((start.0 - r).abs() < 1e-6 && start.1.abs() < 1e-6);
assert!(end.0.abs() < 1e-6 && (end.1 - r).abs() < 1e-6);
}
other => panic!("expected arc, got {other:?}"),
}
}
#[test]
fn classify_wavy_is_polyline_fallback() {
let uv: Vec<(f64, f64)> = (0..=8)
.map(|i| (i as f64, if i % 2 == 0 { 0.0 } else { 3.0 }))
.collect();
assert_eq!(classify_uv(&uv).kind(), "polyline");
}
#[test]
fn add_external_ref_line_marks_points_and_grounds() {
let mut doc = empty_doc();
let link = EdgeLink::Line { a: (1.0, 2.0), b: (7.0, 2.0) };
let (pids, gids) = add_external_ref(&mut doc, &link);
assert_eq!(pids.len(), 2);
assert_eq!(gids.len(), 1);
for id in &pids {
let p = doc.point(id).unwrap();
assert!(p.fixed && p.construction && p.external_reference, "point flags: {p:?}");
let grounds = doc
.constraints
.iter()
.filter(|c| c.ctype() == Some("⏚") && c.points().first().map(id_key) == Some(id_key(id)))
.count();
assert_eq!(grounds, 1, "point {id} should have one ground");
}
let g = doc.geometry(&gids[0]).unwrap();
assert_eq!(g.geom_type, "line");
assert!(g.construction(), "reference geometry must be construction");
}
#[test]
fn link_or_update_dedups_and_updates() {
let mut doc = empty_doc();
let mut refs: Vec<ExternalRef> = Vec::new();
let plane = PlaneFrame::xy();
let poly = [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]];
assert!(link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &poly, &plane));
assert_eq!(refs.len(), 1);
assert_eq!(doc.points.len(), 2);
assert_eq!(doc.geometries.len(), 1);
assert!(!link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &poly, &plane));
assert_eq!(refs.len(), 1);
assert_eq!(doc.points.len(), 2);
assert_eq!(doc.geometries.len(), 1);
let moved = [[0.0, 0.0, 0.0], [10.0, 4.0, 0.0]];
assert!(link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &moved, &plane));
assert_eq!(doc.points.len(), 2);
let p_end = doc.point(&refs[0].point_ids[1]).unwrap();
assert!((p_end.x - 10.0).abs() < 1e-9 && (p_end.y - 4.0).abs() < 1e-9);
let poly2 = [[0.0, 0.0, 0.0], [0.0, 8.0, 0.0]];
assert!(link_or_update(&mut doc, &mut refs, "edgeB", "Solid", &poly2, &plane));
assert_eq!(refs.len(), 2);
assert_eq!(doc.points.len(), 4);
assert_eq!(doc.geometries.len(), 2);
}
#[test]
fn link_or_update_rebuilds_on_structure_change() {
let mut doc = empty_doc();
let mut refs: Vec<ExternalRef> = Vec::new();
let plane = PlaneFrame::xy();
let line = [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]];
link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &line, &plane);
assert_eq!(refs[0].kind, "line");
assert_eq!(doc.points.len(), 2);
let (cx, cy, r) = (0.0, 0.0, 5.0);
let n = 48;
let circle: Vec<[f64; 3]> = (0..=n)
.map(|i| {
let t = i as f64 / n as f64 * std::f64::consts::TAU;
[cx + r * t.cos(), cy + r * t.sin(), 0.0]
})
.collect();
link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &circle, &plane);
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].kind, "circle");
assert_eq!(doc.points.len(), 2);
assert_eq!(doc.geometries.len(), 1);
assert_eq!(doc.geometry(&refs[0].geom_ids[0]).unwrap().geom_type, "circle");
}
#[test]
fn external_ref_json_round_trips() {
let r = ExternalRef {
edge_name: "e".into(),
solid_name: "s".into(),
point_ids: vec![json!(7), json!(8)],
geom_ids: vec![json!(20)],
kind: "line".into(),
};
let v = serde_json::to_value(&r).unwrap();
assert_eq!(v["edgeName"], "e");
assert_eq!(v["pointIds"], json!([7, 8]));
let back: ExternalRef = serde_json::from_value(v).unwrap();
assert_eq!(back, r);
}
}