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
295pub fn prune_dead_refs(doc: &SketchDoc, refs: &mut Vec<ExternalRef>) -> bool {
304 let before = refs.len();
305 refs.retain(|r| {
306 r.point_ids.iter().all(|id| doc.point(id).is_some())
307 && r.geom_ids.iter().all(|id| doc.geometry(id).is_some())
308 });
309 refs.len() != before
310}
311
312fn push_ground(doc: &mut SketchDoc, pid: &Value) {
315 let cid = doc.next_constraint_id();
316 let mut raw = Map::new();
317 raw.insert("id".to_string(), cid);
318 raw.insert("type".to_string(), Value::String("⏚".to_string()));
319 raw.insert("points".to_string(), Value::Array(vec![pid.clone()]));
320 doc.constraints.push(SketchConstraint { raw });
321}
322
323fn push_construction_geometry(doc: &mut SketchDoc, geom_type: &str, points: Vec<Value>) -> Value {
326 let id = doc.next_geometry_id();
327 let mut extra = Map::new();
328 extra.insert("construction".to_string(), Value::Bool(true));
329 doc.geometries.push(SketchGeometry {
330 id: id.clone(),
331 geom_type: geom_type.to_string(),
332 points,
333 extra,
334 });
335 id
336}
337
338fn dist(a: (f64, f64), b: (f64, f64)) -> f64 {
342 ((a.0 - b.0).powi(2) + (a.1 - b.1).powi(2)).sqrt()
343}
344
345fn polyline_extent(uv: &[(f64, f64)]) -> f64 {
347 let (mut minx, mut miny) = (f64::INFINITY, f64::INFINITY);
348 let (mut maxx, mut maxy) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
349 for &(x, y) in uv {
350 minx = minx.min(x);
351 miny = miny.min(y);
352 maxx = maxx.max(x);
353 maxy = maxy.max(y);
354 }
355 ((maxx - minx).powi(2) + (maxy - miny).powi(2)).sqrt()
356}
357
358fn point_segment_distance(p: (f64, f64), a: (f64, f64), b: (f64, f64)) -> f64 {
360 let (dx, dy) = (b.0 - a.0, b.1 - a.1);
361 let len2 = dx * dx + dy * dy;
362 let t = if len2 <= 1e-18 {
363 0.0
364 } else {
365 (((p.0 - a.0) * dx + (p.1 - a.1) * dy) / len2).clamp(0.0, 1.0)
366 };
367 dist(p, (a.0 + t * dx, a.1 + t * dy))
368}
369
370fn fit_circle(p1: (f64, f64), p2: (f64, f64), p3: (f64, f64)) -> Option<(f64, f64, f64)> {
373 let (ax, ay) = p1;
374 let (bx, by) = p2;
375 let (cx, cy) = p3;
376 let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
378 if d.abs() < 1e-12 {
379 return None;
380 }
381 let a2 = ax * ax + ay * ay;
382 let b2 = bx * bx + by * by;
383 let c2 = cx * cx + cy * cy;
384 let ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d;
385 let uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d;
386 let r = dist((ux, uy), p1);
387 if !ux.is_finite() || !uy.is_finite() || !r.is_finite() {
388 return None;
389 }
390 Some((ux, uy, r))
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use serde_json::json;
397
398 fn empty_doc() -> SketchDoc {
399 SketchDoc::default()
400 }
401
402 #[test]
404 fn classify_two_point_polyline_is_a_line() {
405 let link = classify_uv(&[(0.0, 0.0), (10.0, 5.0)]);
406 assert_eq!(link, EdgeLink::Line { a: (0.0, 0.0), b: (10.0, 5.0) });
407 }
408
409 #[test]
412 fn classify_collinear_samples_is_a_line() {
413 let uv: Vec<(f64, f64)> = (0..=10).map(|i| (i as f64, 2.0 * i as f64)).collect();
414 assert_eq!(classify_uv(&uv).kind(), "line");
415 }
416
417 #[test]
420 fn classify_closed_circle() {
421 let (cx, cy, r) = (3.0, -1.0, 5.0);
422 let n = 64;
423 let uv: Vec<(f64, f64)> = (0..=n)
424 .map(|i| {
425 let t = i as f64 / n as f64 * std::f64::consts::TAU;
426 (cx + r * t.cos(), cy + r * t.sin())
427 })
428 .collect();
429 match classify_uv(&uv) {
430 EdgeLink::Circle { center, rim } => {
431 assert!((center.0 - cx).abs() < 1e-6 && (center.1 - cy).abs() < 1e-6);
432 assert!((dist(center, rim) - r).abs() < 1e-6);
433 }
434 other => panic!("expected circle, got {other:?}"),
435 }
436 }
437
438 #[test]
440 fn classify_open_arc() {
441 let (cx, cy, r) = (0.0, 0.0, 4.0);
442 let n = 16;
443 let uv: Vec<(f64, f64)> = (0..=n)
444 .map(|i| {
445 let t = i as f64 / n as f64 * (std::f64::consts::PI / 2.0);
446 (cx + r * t.cos(), cy + r * t.sin())
447 })
448 .collect();
449 match classify_uv(&uv) {
450 EdgeLink::Arc { center, start, end } => {
451 assert!((center.0 - cx).abs() < 1e-6 && (center.1 - cy).abs() < 1e-6);
452 assert!((start.0 - r).abs() < 1e-6 && start.1.abs() < 1e-6);
453 assert!(end.0.abs() < 1e-6 && (end.1 - r).abs() < 1e-6);
454 }
455 other => panic!("expected arc, got {other:?}"),
456 }
457 }
458
459 #[test]
461 fn classify_wavy_is_polyline_fallback() {
462 let uv: Vec<(f64, f64)> = (0..=8)
463 .map(|i| (i as f64, if i % 2 == 0 { 0.0 } else { 3.0 }))
464 .collect();
465 assert_eq!(classify_uv(&uv).kind(), "polyline");
466 }
467
468 #[test]
471 fn add_external_ref_line_marks_points_and_grounds() {
472 let mut doc = empty_doc();
473 let link = EdgeLink::Line { a: (1.0, 2.0), b: (7.0, 2.0) };
474 let (pids, gids) = add_external_ref(&mut doc, &link);
475 assert_eq!(pids.len(), 2);
476 assert_eq!(gids.len(), 1);
477 for id in &pids {
478 let p = doc.point(id).unwrap();
479 assert!(p.fixed && p.construction && p.external_reference, "point flags: {p:?}");
480 let grounds = doc
482 .constraints
483 .iter()
484 .filter(|c| c.ctype() == Some("⏚") && c.points().first().map(id_key) == Some(id_key(id)))
485 .count();
486 assert_eq!(grounds, 1, "point {id} should have one ground");
487 }
488 let g = doc.geometry(&gids[0]).unwrap();
489 assert_eq!(g.geom_type, "line");
490 assert!(g.construction(), "reference geometry must be construction");
491 }
492
493 #[test]
496 fn link_or_update_dedups_and_updates() {
497 let mut doc = empty_doc();
498 let mut refs: Vec<ExternalRef> = Vec::new();
499 let plane = PlaneFrame::xy();
500 let poly = [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]];
501
502 assert!(link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &poly, &plane));
504 assert_eq!(refs.len(), 1);
505 assert_eq!(doc.points.len(), 2);
506 assert_eq!(doc.geometries.len(), 1);
507
508 assert!(!link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &poly, &plane));
510 assert_eq!(refs.len(), 1);
511 assert_eq!(doc.points.len(), 2);
512 assert_eq!(doc.geometries.len(), 1);
513
514 let moved = [[0.0, 0.0, 0.0], [10.0, 4.0, 0.0]];
517 assert!(link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &moved, &plane));
518 assert_eq!(doc.points.len(), 2);
519 let p_end = doc.point(&refs[0].point_ids[1]).unwrap();
520 assert!((p_end.x - 10.0).abs() < 1e-9 && (p_end.y - 4.0).abs() < 1e-9);
521
522 let poly2 = [[0.0, 0.0, 0.0], [0.0, 8.0, 0.0]];
524 assert!(link_or_update(&mut doc, &mut refs, "edgeB", "Solid", &poly2, &plane));
525 assert_eq!(refs.len(), 2);
526 assert_eq!(doc.points.len(), 4);
527 assert_eq!(doc.geometries.len(), 2);
528 }
529
530 #[test]
533 fn link_or_update_rebuilds_on_structure_change() {
534 let mut doc = empty_doc();
535 let mut refs: Vec<ExternalRef> = Vec::new();
536 let plane = PlaneFrame::xy();
537
538 let line = [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]];
539 link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &line, &plane);
540 assert_eq!(refs[0].kind, "line");
541 assert_eq!(doc.points.len(), 2);
542
543 let (cx, cy, r) = (0.0, 0.0, 5.0);
545 let n = 48;
546 let circle: Vec<[f64; 3]> = (0..=n)
547 .map(|i| {
548 let t = i as f64 / n as f64 * std::f64::consts::TAU;
549 [cx + r * t.cos(), cy + r * t.sin(), 0.0]
550 })
551 .collect();
552 link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &circle, &plane);
553 assert_eq!(refs.len(), 1);
554 assert_eq!(refs[0].kind, "circle");
555 assert_eq!(doc.points.len(), 2);
557 assert_eq!(doc.geometries.len(), 1);
558 assert_eq!(doc.geometry(&refs[0].geom_ids[0]).unwrap().geom_type, "circle");
559 }
560
561 #[test]
564 fn prune_dead_refs_drops_only_orphaned_entries() {
565 let mut doc = empty_doc();
566 let mut refs: Vec<ExternalRef> = Vec::new();
567 let plane = PlaneFrame::xy();
568
569 link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]], &plane);
571 link_or_update(&mut doc, &mut refs, "edgeB", "Solid", &[[0.0, 0.0, 0.0], [0.0, 8.0, 0.0]], &plane);
572 assert_eq!(refs.len(), 2);
573
574 assert!(!prune_dead_refs(&doc, &mut refs));
576 assert_eq!(refs.len(), 2);
577
578 remove_ref_entities(&mut doc, &refs[0].clone());
581 assert!(prune_dead_refs(&doc, &mut refs), "orphaned ref pruned");
582 assert_eq!(refs.len(), 1, "only edgeB's live ref survives");
583 assert_eq!(refs[0].edge_name, "edgeB");
584
585 assert!(link_or_update(&mut doc, &mut refs, "edgeA", "Solid", &[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0]], &plane));
587 assert_eq!(refs.len(), 2, "edgeA re-linked as a new ref");
588 }
589
590 #[test]
592 fn external_ref_json_round_trips() {
593 let r = ExternalRef {
594 edge_name: "e".into(),
595 solid_name: "s".into(),
596 point_ids: vec![json!(7), json!(8)],
597 geom_ids: vec![json!(20)],
598 kind: "line".into(),
599 };
600 let v = serde_json::to_value(&r).unwrap();
601 assert_eq!(v["edgeName"], "e");
602 assert_eq!(v["pointIds"], json!([7, 8]));
603 let back: ExternalRef = serde_json::from_value(v).unwrap();
604 assert_eq!(back, r);
605 }
606}