1use crate::geometry2d::{distance as dist, point_segment_distance};
18
19use std::collections::HashSet;
20
21use serde::{Deserialize, Serialize};
22use serde_json::{Map, Value};
23
24use super::doc::{id_key, SketchConstraint, SketchDoc, SketchGeometry, SketchPoint};
25use super::PlaneFrame;
26
27#[derive(Clone, Debug, PartialEq)]
29pub enum EdgeLink {
30 Line { a: (f64, f64), b: (f64, f64) },
32 Circle { center: (f64, f64), rim: (f64, f64) },
34 Arc {
39 center: (f64, f64),
40 start: (f64, f64),
41 end: (f64, f64),
42 },
43 Polyline { pts: Vec<(f64, f64)> },
46}
47
48impl EdgeLink {
49 pub fn kind(&self) -> &'static str {
51 match self {
52 EdgeLink::Line { .. } => "line",
53 EdgeLink::Circle { .. } => "circle",
54 EdgeLink::Arc { .. } => "arc",
55 EdgeLink::Polyline { .. } => "polyline",
56 }
57 }
58
59 pub fn point_uvs(&self) -> Vec<(f64, f64)> {
62 match self {
63 EdgeLink::Line { a, b } => vec![*a, *b],
64 EdgeLink::Circle { center, rim } => vec![*center, *rim],
65 EdgeLink::Arc { center, start, end } => vec![*center, *start, *end],
66 EdgeLink::Polyline { pts } => pts.clone(),
67 }
68 }
69}
70
71pub fn project_polyline(plane: &PlaneFrame, world: &[[f64; 3]]) -> Vec<(f64, f64)> {
75 world.iter().map(|&w| plane.to_uv(w)).collect()
76}
77
78pub fn classify_uv(uv: &[(f64, f64)]) -> EdgeLink {
89 let n = uv.len();
90 if n < 2 {
91 return EdgeLink::Polyline { pts: uv.to_vec() };
94 }
95 let a = uv[0];
96 let b = uv[n - 1];
97 let extent = polyline_extent(uv).max(1e-9);
98 let straight_tol = 1e-4 * extent;
99 let closed = dist(a, b) <= straight_tol;
100
101 if !closed {
103 let max_dev = uv[1..n - 1]
104 .iter()
105 .map(|&p| point_segment_distance(p, a, b).0)
106 .fold(0.0_f64, f64::max);
107 if n == 2 || max_dev <= straight_tol {
108 return EdgeLink::Line { a, b };
109 }
110 }
111
112 if n >= 3 {
116 if let Some((cx, cy, r)) = fit_circle(uv[0], uv[n / 3], uv[(2 * n) / 3]) {
117 let circle_tol = 1e-3 * extent;
118 let residual = uv
119 .iter()
120 .map(|&p| (dist(p, (cx, cy)) - r).abs())
121 .fold(0.0_f64, f64::max);
122 if r.is_finite() && r > straight_tol && residual <= circle_tol {
123 if closed {
124 return EdgeLink::Circle {
125 center: (cx, cy),
126 rim: (cx + r, cy),
127 };
128 }
129 let (start, end) = if winding_ccw(uv, (cx, cy)) { (a, b) } else { (b, a) };
134 return EdgeLink::Arc {
135 center: (cx, cy),
136 start,
137 end,
138 };
139 }
140 }
141 }
142
143 EdgeLink::Polyline { pts: uv.to_vec() }
144}
145
146#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
150pub struct ExternalRef {
151 #[serde(rename = "edgeName")]
153 pub edge_name: String,
154 #[serde(rename = "solidName", default)]
156 pub solid_name: String,
157 #[serde(rename = "pointIds", default)]
159 pub point_ids: Vec<Value>,
160 #[serde(rename = "geomIds", default)]
163 pub geom_ids: Vec<Value>,
164 #[serde(default)]
166 pub kind: String,
167}
168
169pub fn link_or_update(
182 doc: &mut SketchDoc,
183 refs: &mut Vec<ExternalRef>,
184 edge_name: &str,
185 solid_name: &str,
186 world_poly: &[[f64; 3]],
187 plane: &PlaneFrame,
188) -> bool {
189 if world_poly.len() < 2 {
190 return false;
191 }
192 let uv = project_polyline(plane, world_poly);
193 let link = classify_uv(&uv);
194 let new_uvs = link.point_uvs();
195
196 if let Some(pos) = refs.iter().position(|r| r.edge_name == edge_name) {
197 let structure_matches =
198 refs[pos].kind == link.kind() && refs[pos].point_ids.len() == new_uvs.len();
199 if structure_matches {
200 let mut moved = false;
202 let point_ids = refs[pos].point_ids.clone();
203 for (id, (u, v)) in point_ids.iter().zip(new_uvs.iter()) {
204 if let Some(p) = doc.point_mut(id) {
205 if (p.x - u).abs() > 1e-12 || (p.y - v).abs() > 1e-12 {
206 moved = true;
207 }
208 p.x = *u;
209 p.y = *v;
210 p.fixed = true;
211 p.construction = true;
212 p.external_reference = true;
213 }
214 }
215 if refs[pos].solid_name != solid_name {
216 refs[pos].solid_name = solid_name.to_string();
217 }
218 return moved;
219 }
220 remove_ref_entities(doc, &refs[pos].clone());
223 let (point_ids, geom_ids) = add_external_ref(doc, &link);
224 refs[pos] = ExternalRef {
225 edge_name: edge_name.to_string(),
226 solid_name: solid_name.to_string(),
227 point_ids,
228 geom_ids,
229 kind: link.kind().to_string(),
230 };
231 return true;
232 }
233
234 let (point_ids, geom_ids) = add_external_ref(doc, &link);
236 refs.push(ExternalRef {
237 edge_name: edge_name.to_string(),
238 solid_name: solid_name.to_string(),
239 point_ids,
240 geom_ids,
241 kind: link.kind().to_string(),
242 });
243 true
244}
245
246pub fn add_external_ref(doc: &mut SketchDoc, link: &EdgeLink) -> (Vec<Value>, Vec<Value>) {
250 let mut point_ids = Vec::new();
251 for (u, v) in link.point_uvs() {
252 let id = doc.next_point_id();
253 doc.points.push(SketchPoint {
254 id: id.clone(),
255 x: u,
256 y: v,
257 fixed: true,
258 construction: true,
259 external_reference: true,
260 });
261 push_ground(doc, &id);
262 point_ids.push(id);
263 }
264 let geom_ids = match link {
265 EdgeLink::Line { .. } => vec![push_construction_geometry(
266 doc,
267 "line",
268 vec![point_ids[0].clone(), point_ids[1].clone()],
269 )],
270 EdgeLink::Circle { .. } => vec![push_construction_geometry(
271 doc,
272 "circle",
273 vec![point_ids[0].clone(), point_ids[1].clone()],
274 )],
275 EdgeLink::Arc { .. } => vec![push_construction_geometry(
276 doc,
277 "arc",
278 vec![
279 point_ids[0].clone(),
280 point_ids[1].clone(),
281 point_ids[2].clone(),
282 ],
283 )],
284 EdgeLink::Polyline { .. } => point_ids
285 .windows(2)
286 .map(|w| push_construction_geometry(doc, "line", vec![w[0].clone(), w[1].clone()]))
287 .collect(),
288 };
289 (point_ids, geom_ids)
290}
291
292pub fn add_copied_edge(doc: &mut SketchDoc, link: &EdgeLink, note: &str) -> Vec<Value> {
299 let uvs = link.point_uvs();
300 let near = 1e-6 * polyline_extent(&uvs).max(1.0);
301 let mut point_ids = Vec::new();
302 for (u, v) in uvs {
303 let reuse = doc
304 .points
305 .iter()
306 .find(|p| !p.construction && !p.external_reference && dist((p.x, p.y), (u, v)) <= near)
307 .map(|p| p.id.clone());
308 let id = reuse.unwrap_or_else(|| {
309 let id = doc.next_point_id();
310 doc.points.push(SketchPoint { id: id.clone(), x: u, y: v, fixed: false, construction: false, external_reference: false });
311 id
312 });
313 point_ids.push(id);
314 }
315 let push = |doc: &mut SketchDoc, geom_type: &str, points: Vec<Value>| {
316 let id = doc.next_geometry_id();
317 let mut extra = Map::new();
318 extra.insert("note".to_string(), Value::String(note.to_string()));
319 doc.geometries.push(SketchGeometry { id: id.clone(), geom_type: geom_type.to_string(), points, extra });
320 id
321 };
322 match link {
323 EdgeLink::Line { .. } => vec![push(doc, "line", point_ids[..2].to_vec())],
324 EdgeLink::Circle { .. } => vec![push(doc, "circle", point_ids[..2].to_vec())],
325 EdgeLink::Arc { .. } => vec![push(doc, "arc", point_ids[..3].to_vec())],
326 EdgeLink::Polyline { .. } => point_ids
327 .windows(2)
328 .filter(|w| id_key(&w[0]) != id_key(&w[1]))
329 .map(|w| push(doc, "line", w.to_vec()))
330 .collect(),
331 }
332}
333
334fn remove_ref_entities(doc: &mut SketchDoc, r: &ExternalRef) {
337 let pt_keys: HashSet<String> = r.point_ids.iter().map(id_key).collect();
338 let geo_keys: HashSet<String> = r.geom_ids.iter().map(id_key).collect();
339 doc.geometries.retain(|g| !geo_keys.contains(&id_key(&g.id)));
340 doc.points.retain(|p| !pt_keys.contains(&id_key(&p.id)));
341 doc.constraints
342 .retain(|c| !c.points().iter().any(|p| pt_keys.contains(&id_key(p))));
343}
344
345pub fn prune_dead_refs(doc: &SketchDoc, refs: &mut Vec<ExternalRef>) -> bool {
354 let before = refs.len();
355 refs.retain(|r| {
356 r.point_ids.iter().all(|id| doc.point(id).is_some())
357 && r.geom_ids.iter().all(|id| doc.geometry(id).is_some())
358 });
359 refs.len() != before
360}
361
362pub const LINKS_LISTED_KEY: &str = "linksInProjectedEdges";
368
369pub fn release_ref(doc: &mut SketchDoc, r: &ExternalRef) -> bool {
376 let mut changed = false;
377 for id in &r.point_ids {
378 if let Some(p) = doc.point_mut(id) {
379 if p.external_reference || !p.fixed {
380 changed = true;
381 }
382 p.external_reference = false;
383 p.fixed = true;
384 }
385 }
386 changed
387}
388
389pub fn release_ref_json(sketch: &mut Value, r: &ExternalRef) -> bool {
395 let keys: HashSet<String> = r.point_ids.iter().map(id_key).collect();
396 let Some(points) = sketch.get_mut("points").and_then(Value::as_array_mut) else {
397 return false;
398 };
399 let mut changed = false;
400 for point in points.iter_mut() {
401 if !point.get("id").is_some_and(|id| keys.contains(&id_key(id))) {
402 continue;
403 }
404 if point.get("externalReference").and_then(Value::as_bool) != Some(false) {
405 changed = true;
406 }
407 if point.get("fixed").and_then(Value::as_bool) != Some(true) {
408 changed = true;
409 }
410 point["externalReference"] = Value::Bool(false);
411 point["fixed"] = Value::Bool(true);
412 }
413 changed
414}
415
416pub fn listed_edge_names(
423 listed: &[String],
424 refs: &[ExternalRef],
425 kept_unlinked: &[String],
426) -> Vec<String> {
427 let mut out: Vec<String> = listed
428 .iter()
429 .filter(|name| {
430 refs.iter().any(|r| &r.edge_name == *name) || kept_unlinked.contains(name)
431 })
432 .cloned()
433 .collect();
434 for r in refs {
435 if !out.contains(&r.edge_name) {
436 out.push(r.edge_name.clone());
437 }
438 }
439 out
440}
441
442pub fn ensure_origin_point(doc: &mut SketchDoc, plane: &PlaneFrame) -> bool {
450 let id = Value::String(brep_kernel::SKETCH_ORIGIN_POINT_ID.to_string());
451 let (u, v) = plane.to_uv([0.0, 0.0, 0.0]);
452 let mut changed = false;
453 match doc.point_mut(&id) {
454 Some(p) => {
455 if (p.x - u).abs() > 1e-12 || (p.y - v).abs() > 1e-12 {
456 changed = true;
457 }
458 if !(p.fixed && p.construction && p.external_reference) {
459 changed = true;
460 }
461 p.x = u;
462 p.y = v;
463 p.fixed = true;
464 p.construction = true;
465 p.external_reference = true;
466 }
467 None => {
468 doc.points.push(SketchPoint {
469 id: id.clone(),
470 x: u,
471 y: v,
472 fixed: true,
473 construction: true,
474 external_reference: true,
475 });
476 changed = true;
477 }
478 }
479 let grounded = doc.constraints.iter().any(|c| {
480 c.ctype() == Some("⏚") && c.points().first().map(id_key) == Some(id_key(&id))
481 });
482 if !grounded {
483 let mut raw = Map::new();
484 raw.insert("id".to_string(), id.clone());
485 raw.insert("type".to_string(), Value::String("⏚".to_string()));
486 raw.insert("points".to_string(), Value::Array(vec![id]));
487 doc.constraints.push(SketchConstraint { raw });
488 changed = true;
489 }
490 changed
491}
492
493pub fn is_origin_id(id: &Value) -> bool {
495 brep_kernel::is_sketch_origin_point_id(id)
496}
497
498fn push_ground(doc: &mut SketchDoc, pid: &Value) {
501 let cid = doc.next_constraint_id();
502 let mut raw = Map::new();
503 raw.insert("id".to_string(), cid);
504 raw.insert("type".to_string(), Value::String("⏚".to_string()));
505 raw.insert("points".to_string(), Value::Array(vec![pid.clone()]));
506 doc.constraints.push(SketchConstraint { raw });
507}
508
509fn push_construction_geometry(doc: &mut SketchDoc, geom_type: &str, points: Vec<Value>) -> Value {
512 let id = doc.next_geometry_id();
513 let mut extra = Map::new();
514 extra.insert("construction".to_string(), Value::Bool(true));
515 doc.geometries.push(SketchGeometry {
516 id: id.clone(),
517 geom_type: geom_type.to_string(),
518 points,
519 extra,
520 });
521 id
522}
523
524fn polyline_extent(uv: &[(f64, f64)]) -> f64 {
528 let (mut minx, mut miny) = (f64::INFINITY, f64::INFINITY);
529 let (mut maxx, mut maxy) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
530 for &(x, y) in uv {
531 minx = minx.min(x);
532 miny = miny.min(y);
533 maxx = maxx.max(x);
534 maxy = maxy.max(y);
535 }
536 ((maxx - minx).powi(2) + (maxy - miny).powi(2)).sqrt()
537}
538
539fn winding_ccw(uv: &[(f64, f64)], center: (f64, f64)) -> bool {
544 use std::f64::consts::{PI, TAU};
545 let angle = |p: (f64, f64)| (p.1 - center.1).atan2(p.0 - center.0);
546 let total: f64 = uv
547 .windows(2)
548 .map(|w| {
549 let step = angle(w[1]) - angle(w[0]);
550 step - TAU * ((step + PI) / TAU).floor()
553 })
554 .sum();
555 total >= 0.0
556}
557
558fn fit_circle(p1: (f64, f64), p2: (f64, f64), p3: (f64, f64)) -> Option<(f64, f64, f64)> {
561 let (ax, ay) = p1;
562 let (bx, by) = p2;
563 let (cx, cy) = p3;
564 let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
566 if d.abs() < 1e-12 {
567 return None;
568 }
569 let a2 = ax * ax + ay * ay;
570 let b2 = bx * bx + by * by;
571 let c2 = cx * cx + cy * cy;
572 let ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d;
573 let uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d;
574 let r = dist((ux, uy), p1);
575 if !ux.is_finite() || !uy.is_finite() || !r.is_finite() {
576 return None;
577 }
578 Some((ux, uy, r))
579}
580