use crate::geometry2d::{distance as dist, point_segment_distance};
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).0)
.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))));
}
pub fn prune_dead_refs(doc: &SketchDoc, refs: &mut Vec<ExternalRef>) -> bool {
let before = refs.len();
refs.retain(|r| {
r.point_ids.iter().all(|id| doc.point(id).is_some())
&& r.geom_ids.iter().all(|id| doc.geometry(id).is_some())
});
refs.len() != before
}
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 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 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))
}