1use std::collections::HashSet;
19
20use serde::{Deserialize, Serialize};
21use serde_json::{Map, Value};
22
23use super::doc::{id_key, SketchConstraint, SketchDoc, SketchGeometry, SketchPoint};
24use super::PlaneFrame;
25
26#[derive(Clone, Debug, PartialEq)]
28pub enum EdgeLink {
29 Line { a: (f64, f64), b: (f64, f64) },
31 Circle { center: (f64, f64), rim: (f64, f64) },
33 Arc {
35 center: (f64, f64),
36 start: (f64, f64),
37 end: (f64, f64),
38 },
39 Polyline { pts: Vec<(f64, f64)> },
42}
43
44impl EdgeLink {
45 pub fn kind(&self) -> &'static str {
48 match self {
49 EdgeLink::Line { .. } => "line",
50 EdgeLink::Circle { .. } => "circle",
51 EdgeLink::Arc { .. } => "arc",
52 EdgeLink::Polyline { .. } => "polyline",
53 }
54 }
55
56 pub fn point_uvs(&self) -> Vec<(f64, f64)> {
59 match self {
60 EdgeLink::Line { a, b } => vec![*a, *b],
61 EdgeLink::Circle { center, rim } => vec![*center, *rim],
62 EdgeLink::Arc { center, start, end } => vec![*center, *start, *end],
63 EdgeLink::Polyline { pts } => pts.clone(),
64 }
65 }
66}
67
68pub fn project_polyline(plane: &PlaneFrame, world: &[[f64; 3]]) -> Vec<(f64, f64)> {
72 world.iter().map(|&w| plane.to_uv(w)).collect()
73}
74
75pub fn classify_uv(uv: &[(f64, f64)]) -> EdgeLink {
86 let n = uv.len();
87 if n < 2 {
88 return EdgeLink::Polyline { pts: uv.to_vec() };
91 }
92 let a = uv[0];
93 let b = uv[n - 1];
94 let extent = polyline_extent(uv).max(1e-9);
95 let straight_tol = 1e-4 * extent;
96 let closed = dist(a, b) <= straight_tol;
97
98 if !closed {
100 let max_dev = uv[1..n - 1]
101 .iter()
102 .map(|&p| point_segment_distance(p, a, b))
103 .fold(0.0_f64, f64::max);
104 if n == 2 || max_dev <= straight_tol {
105 return EdgeLink::Line { a, b };
106 }
107 }
108
109 if n >= 3 {
113 if let Some((cx, cy, r)) = fit_circle(uv[0], uv[n / 3], uv[(2 * n) / 3]) {
114 let circle_tol = 1e-3 * extent;
115 let residual = uv
116 .iter()
117 .map(|&p| (dist(p, (cx, cy)) - r).abs())
118 .fold(0.0_f64, f64::max);
119 if r.is_finite() && r > straight_tol && residual <= circle_tol {
120 if closed {
121 return EdgeLink::Circle {
122 center: (cx, cy),
123 rim: (cx + r, cy),
124 };
125 }
126 return EdgeLink::Arc {
127 center: (cx, cy),
128 start: a,
129 end: b,
130 };
131 }
132 }
133 }
134
135 EdgeLink::Polyline { pts: uv.to_vec() }
136}
137
138#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
142pub struct ExternalRef {
143 #[serde(rename = "edgeName")]
145 pub edge_name: String,
146 #[serde(rename = "solidName", default)]
148 pub solid_name: String,
149 #[serde(rename = "pointIds", default)]
151 pub point_ids: Vec<Value>,
152 #[serde(rename = "geomIds", default)]
155 pub geom_ids: Vec<Value>,
156 #[serde(default)]
158 pub kind: String,
159}
160
161pub fn link_or_update(
174 doc: &mut SketchDoc,
175 refs: &mut Vec<ExternalRef>,
176 edge_name: &str,
177 solid_name: &str,
178 world_poly: &[[f64; 3]],
179 plane: &PlaneFrame,
180) -> bool {
181 if world_poly.len() < 2 {
182 return false;
183 }
184 let uv = project_polyline(plane, world_poly);
185 let link = classify_uv(&uv);
186 let new_uvs = link.point_uvs();
187
188 if let Some(pos) = refs.iter().position(|r| r.edge_name == edge_name) {
189 let structure_matches =
190 refs[pos].kind == link.kind() && refs[pos].point_ids.len() == new_uvs.len();
191 if structure_matches {
192 let mut moved = false;
194 let point_ids = refs[pos].point_ids.clone();
195 for (id, (u, v)) in point_ids.iter().zip(new_uvs.iter()) {
196 if let Some(p) = doc.point_mut(id) {
197 if (p.x - u).abs() > 1e-12 || (p.y - v).abs() > 1e-12 {
198 moved = true;
199 }
200 p.x = *u;
201 p.y = *v;
202 p.fixed = true;
203 p.construction = true;
204 p.external_reference = true;
205 }
206 }
207 if refs[pos].solid_name != solid_name {
208 refs[pos].solid_name = solid_name.to_string();
209 }
210 return moved;
211 }
212 remove_ref_entities(doc, &refs[pos].clone());
215 let (point_ids, geom_ids) = add_external_ref(doc, &link);
216 refs[pos] = ExternalRef {
217 edge_name: edge_name.to_string(),
218 solid_name: solid_name.to_string(),
219 point_ids,
220 geom_ids,
221 kind: link.kind().to_string(),
222 };
223 return true;
224 }
225
226 let (point_ids, geom_ids) = add_external_ref(doc, &link);
228 refs.push(ExternalRef {
229 edge_name: edge_name.to_string(),
230 solid_name: solid_name.to_string(),
231 point_ids,
232 geom_ids,
233 kind: link.kind().to_string(),
234 });
235 true
236}
237
238pub fn add_external_ref(doc: &mut SketchDoc, link: &EdgeLink) -> (Vec<Value>, Vec<Value>) {
242 let mut point_ids = Vec::new();
243 for (u, v) in link.point_uvs() {
244 let id = doc.next_point_id();
245 doc.points.push(SketchPoint {
246 id: id.clone(),
247 x: u,
248 y: v,
249 fixed: true,
250 construction: true,
251 external_reference: true,
252 });
253 push_ground(doc, &id);
254 point_ids.push(id);
255 }
256 let geom_ids = match link {
257 EdgeLink::Line { .. } => vec![push_construction_geometry(
258 doc,
259 "line",
260 vec![point_ids[0].clone(), point_ids[1].clone()],
261 )],
262 EdgeLink::Circle { .. } => vec![push_construction_geometry(
263 doc,
264 "circle",
265 vec![point_ids[0].clone(), point_ids[1].clone()],
266 )],
267 EdgeLink::Arc { .. } => vec![push_construction_geometry(
268 doc,
269 "arc",
270 vec![
271 point_ids[0].clone(),
272 point_ids[1].clone(),
273 point_ids[2].clone(),
274 ],
275 )],
276 EdgeLink::Polyline { .. } => point_ids
277 .windows(2)
278 .map(|w| push_construction_geometry(doc, "line", vec![w[0].clone(), w[1].clone()]))
279 .collect(),
280 };
281 (point_ids, geom_ids)
282}
283
284fn remove_ref_entities(doc: &mut SketchDoc, r: &ExternalRef) {
287 let pt_keys: HashSet<String> = r.point_ids.iter().map(id_key).collect();
288 let geo_keys: HashSet<String> = r.geom_ids.iter().map(id_key).collect();
289 doc.geometries.retain(|g| !geo_keys.contains(&id_key(&g.id)));
290 doc.points.retain(|p| !pt_keys.contains(&id_key(&p.id)));
291 doc.constraints
292 .retain(|c| !c.points().iter().any(|p| pt_keys.contains(&id_key(p))));
293}
294
295fn push_ground(doc: &mut SketchDoc, pid: &Value) {
298 let cid = doc.next_constraint_id();
299 let mut raw = Map::new();
300 raw.insert("id".to_string(), cid);
301 raw.insert("type".to_string(), Value::String("⏚".to_string()));
302 raw.insert("points".to_string(), Value::Array(vec![pid.clone()]));
303 doc.constraints.push(SketchConstraint { raw });
304}
305
306fn push_construction_geometry(doc: &mut SketchDoc, geom_type: &str, points: Vec<Value>) -> Value {
309 let id = doc.next_geometry_id();
310 let mut extra = Map::new();
311 extra.insert("construction".to_string(), Value::Bool(true));
312 doc.geometries.push(SketchGeometry {
313 id: id.clone(),
314 geom_type: geom_type.to_string(),
315 points,
316 extra,
317 });
318 id
319}
320
321fn dist(a: (f64, f64), b: (f64, f64)) -> f64 {
325 ((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt()
326}
327
328fn polyline_extent(uv: &[(f64, f64)]) -> f64 {
330 let (mut minx, mut miny) = (f64::INFINITY, f64::INFINITY);
331 let (mut maxx, mut maxy) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
332 for &(x, y) in uv {
333 minx = minx.min(x);
334 miny = miny.min(y);
335 maxx = maxx.max(x);
336 maxy = maxy.max(y);
337 }
338 ((maxx - minx).powi(2) + (maxy - miny).powi(2)).sqrt()
339}
340
341fn point_segment_distance(p: (f64, f64), a: (f64, f64), b: (f64, f64)) -> f64 {
343 let (dx, dy) = (b.0 - a.0, b.1 - a.1);
344 let len2 = dx * dx + dy * dy;
345 let t = if len2 <= 1e-18 {
346 0.0
347 } else {
348 (((p.0 - a.0) * dx + (p.1 - a.1) * dy) / len2).clamp(0.0, 1.0)
349 };
350 dist(p, (a.0 + t * dx, a.1 + t * dy))
351}
352
353fn fit_circle(p1: (f64, f64), p2: (f64, f64), p3: (f64, f64)) -> Option<(f64, f64, f64)> {
356 let (ax, ay) = p1;
357 let (bx, by) = p2;
358 let (cx, cy) = p3;
359 let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
361 if d.abs() < 1e-12 {
362 return None;
363 }
364 let a2 = ax * ax + ay * ay;
365 let b2 = bx * bx + by * by;
366 let c2 = cx * cx + cy * cy;
367 let ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d;
368 let uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d;
369 let r = dist((ux, uy), p1);
370 if !ux.is_finite() || !uy.is_finite() || !r.is_finite() {
371 return None;
372 }
373 Some((ux, uy, r))
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use serde_json::json;
380
381 fn empty_doc() -> SketchDoc {
382 SketchDoc::default()
383 }
384
385 #[test]
387 fn classify_two_point_polyline_is_a_line() {
388 let link = classify_uv(&[(0.0, 0.0), (10.0, 5.0)]);
389 assert_eq!(link, EdgeLink::Line { a: (0.0, 0.0), b: (10.0, 5.0) });
390 }
391
392 #[test]
395 fn classify_collinear_samples_is_a_line() {
396 let uv: Vec<(f64, f64)> = (0..=10).map(|i| (i as f64, 2.0 * i as f64)).collect();
397 assert_eq!(classify_uv(&uv).kind(), "line");
398 }
399
400 #[test]
403 fn classify_closed_circle() {
404 let (cx, cy, r) = (3.0, -1.0, 5.0);
405 let n = 64;
406 let uv: Vec<(f64, f64)> = (0..=n)
407 .map(|i| {
408 let t = i as f64 / n as f64 * std::f64::consts::TAU;
409 (cx + r * t.cos(), cy + r * t.sin())
410 })
411 .collect();
412 match classify_uv(&uv) {
413 EdgeLink::Circle { center, rim } => {
414 assert!((center.0 - cx).abs() < 1e-6 && (center.1 - cy).abs() < 1e-6);
415 assert!((dist(center, rim) - r).abs() < 1e-6);
416 }
417 other => panic!("expected circle, got {other:?}"),
418 }
419 }
420
421 #[test]
423 fn classify_open_arc() {
424 let (cx, cy, r) = (0.0, 0.0, 4.0);
425 let n = 16;
426 let uv: Vec<(f64, f64)> = (0..=n)
427 .map(|i| {
428 let t = i as f64 / n as f64 * (std::f64::consts::PI / 2.0);
429 (cx + r * t.cos(), cy + r * t.sin())
430 })
431 .collect();
432 match classify_uv(&uv) {
433 EdgeLink::Arc { center, start, end } => {
434 assert!((center.0 - cx).abs() < 1e-6 && (center.1 - cy).abs() < 1e-6);
435 assert!((start.0 - r).abs() < 1e-6 && start.1.abs() < 1e-6);
436 assert!(end.0.abs() < 1e-6 && (end.1 - r).abs() < 1e-6);
437 }
438 other => panic!("expected arc, got {other:?}"),
439 }
440 }
441
442 #[test]
444 fn classify_wavy_is_polyline_fallback() {
445 let uv: Vec<(f64, f64)> = (0..=8)
446 .map(|i| (i as f64, if i % 2 == 0 { 0.0 } else { 3.0 }))
447 .collect();
448 assert_eq!(classify_uv(&uv).kind(), "polyline");
449 }
450
451 #[test]
454 fn add_external_ref_line_marks_points_and_grounds() {
455 let mut doc = empty_doc();
456 let link = EdgeLink::Line { a: (1.0, 2.0), b: (7.0, 2.0) };
457 let (pids, gids) = add_external_ref(&mut doc, &link);
458 assert_eq!(pids.len(), 2);
459 assert_eq!(gids.len(), 1);
460 for id in &pids {
461 let p = doc.point(id).unwrap();
462 assert!(p.fixed && p.construction && p.external_reference, "point flags: {p:?}");
463 let grounds = doc
465 .constraints
466 .iter()
467 .filter(|c| c.ctype() == Some("⏚") && c.points().first().map(id_key) == Some(id_key(id)))
468 .count();
469 assert_eq!(grounds, 1, "point {id} should have one ground");
470 }
471 let g = doc.geometry(&gids[0]).unwrap();
472 assert_eq!(g.geom_type, "line");
473 assert!(g.construction(), "reference geometry must be construction");
474 }
475
476 #[test]
479 fn link_or_update_dedups_and_updates() {
480 let mut doc = empty_doc();
481 let mut refs: Vec<ExternalRef> = Vec::new();
482 let plane = PlaneFrame::xy();
483 let poly = [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]];
484
485 assert!(link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &poly, &plane));
487 assert_eq!(refs.len(), 1);
488 assert_eq!(doc.points.len(), 2);
489 assert_eq!(doc.geometries.len(), 1);
490
491 assert!(!link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &poly, &plane));
493 assert_eq!(refs.len(), 1);
494 assert_eq!(doc.points.len(), 2);
495 assert_eq!(doc.geometries.len(), 1);
496
497 let moved = [[0.0, 0.0, 0.0], [10.0, 4.0, 0.0]];
500 assert!(link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &moved, &plane));
501 assert_eq!(doc.points.len(), 2);
502 let p_end = doc.point(&refs[0].point_ids[1]).unwrap();
503 assert!((p_end.x - 10.0).abs() < 1e-9 && (p_end.y - 4.0).abs() < 1e-9);
504
505 let poly2 = [[0.0, 0.0, 0.0], [0.0, 8.0, 0.0]];
507 assert!(link_or_update(&mut doc, &mut refs, "edgeB", "Solid", &poly2, &plane));
508 assert_eq!(refs.len(), 2);
509 assert_eq!(doc.points.len(), 4);
510 assert_eq!(doc.geometries.len(), 2);
511 }
512
513 #[test]
516 fn link_or_update_rebuilds_on_structure_change() {
517 let mut doc = empty_doc();
518 let mut refs: Vec<ExternalRef> = Vec::new();
519 let plane = PlaneFrame::xy();
520
521 let line = [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]];
522 link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &line, &plane);
523 assert_eq!(refs[0].kind, "line");
524 assert_eq!(doc.points.len(), 2);
525
526 let (cx, cy, r) = (0.0, 0.0, 5.0);
528 let n = 48;
529 let circle: Vec<[f64; 3]> = (0..=n)
530 .map(|i| {
531 let t = i as f64 / n as f64 * std::f64::consts::TAU;
532 [cx + r * t.cos(), cy + r * t.sin(), 0.0]
533 })
534 .collect();
535 link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &circle, &plane);
536 assert_eq!(refs.len(), 1);
537 assert_eq!(refs[0].kind, "circle");
538 assert_eq!(doc.points.len(), 2);
540 assert_eq!(doc.geometries.len(), 1);
541 assert_eq!(doc.geometry(&refs[0].geom_ids[0]).unwrap().geom_type, "circle");
542 }
543
544 #[test]
546 fn external_ref_json_round_trips() {
547 let r = ExternalRef {
548 edge_name: "e".into(),
549 solid_name: "s".into(),
550 point_ids: vec![json!(7), json!(8)],
551 geom_ids: vec![json!(20)],
552 kind: "line".into(),
553 };
554 let v = serde_json::to_value(&r).unwrap();
555 assert_eq!(v["edgeName"], "e");
556 assert_eq!(v["pointIds"], json!([7, 8]));
557 let back: ExternalRef = serde_json::from_value(v).unwrap();
558 assert_eq!(back, r);
559 }
560}