1use crate::geometry2d::{distance as dist, point_segment_distance};
8
9use std::collections::HashSet;
10
11use serde::{Deserialize, Serialize};
12use serde_json::{Map, Value};
13
14use super::doc::{id_key, SketchConstraint, SketchDoc, SketchGeometry, SketchPoint};
15use super::PlaneFrame;
16
17#[derive(Clone, Debug, PartialEq)]
19pub enum EdgeLink {
20 Line { a: (f64, f64), b: (f64, f64) },
22 Circle { center: (f64, f64), rim: (f64, f64) },
24 Arc {
26 center: (f64, f64),
27 start: (f64, f64),
28 end: (f64, f64),
29 },
30 Polyline { pts: Vec<(f64, f64)> },
33}
34
35impl EdgeLink {
36 pub fn kind(&self) -> &'static str {
38 match self {
39 EdgeLink::Line { .. } => "line",
40 EdgeLink::Circle { .. } => "circle",
41 EdgeLink::Arc { .. } => "arc",
42 EdgeLink::Polyline { .. } => "polyline",
43 }
44 }
45
46 pub fn point_uvs(&self) -> Vec<(f64, f64)> {
49 match self {
50 EdgeLink::Line { a, b } => vec![*a, *b],
51 EdgeLink::Circle { center, rim } => vec![*center, *rim],
52 EdgeLink::Arc { center, start, end } => vec![*center, *start, *end],
53 EdgeLink::Polyline { pts } => pts.clone(),
54 }
55 }
56}
57
58pub fn project_polyline(plane: &PlaneFrame, world: &[[f64; 3]]) -> Vec<(f64, f64)> {
62 world.iter().map(|&w| plane.to_uv(w)).collect()
63}
64
65pub fn classify_uv(uv: &[(f64, f64)]) -> EdgeLink {
76 let n = uv.len();
77 if n < 2 {
78 return EdgeLink::Polyline { pts: uv.to_vec() };
81 }
82 let a = uv[0];
83 let b = uv[n - 1];
84 let extent = polyline_extent(uv).max(1e-9);
85 let straight_tol = 1e-4 * extent;
86 let closed = dist(a, b) <= straight_tol;
87
88 if !closed {
90 let max_dev = uv[1..n - 1]
91 .iter()
92 .map(|&p| point_segment_distance(p, a, b).0)
93 .fold(0.0_f64, f64::max);
94 if n == 2 || max_dev <= straight_tol {
95 return EdgeLink::Line { a, b };
96 }
97 }
98
99 if n >= 3 {
103 if let Some((cx, cy, r)) = fit_circle(uv[0], uv[n / 3], uv[(2 * n) / 3]) {
104 let circle_tol = 1e-3 * extent;
105 let residual = uv
106 .iter()
107 .map(|&p| (dist(p, (cx, cy)) - r).abs())
108 .fold(0.0_f64, f64::max);
109 if r.is_finite() && r > straight_tol && residual <= circle_tol {
110 if closed {
111 return EdgeLink::Circle {
112 center: (cx, cy),
113 rim: (cx + r, cy),
114 };
115 }
116 return EdgeLink::Arc {
117 center: (cx, cy),
118 start: a,
119 end: b,
120 };
121 }
122 }
123 }
124
125 EdgeLink::Polyline { pts: uv.to_vec() }
126}
127
128#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
132pub struct ExternalRef {
133 #[serde(rename = "edgeName")]
135 pub edge_name: String,
136 #[serde(rename = "solidName", default)]
138 pub solid_name: String,
139 #[serde(rename = "pointIds", default)]
141 pub point_ids: Vec<Value>,
142 #[serde(rename = "geomIds", default)]
145 pub geom_ids: Vec<Value>,
146 #[serde(default)]
148 pub kind: String,
149}
150
151pub fn link_or_update(
164 doc: &mut SketchDoc,
165 refs: &mut Vec<ExternalRef>,
166 edge_name: &str,
167 solid_name: &str,
168 world_poly: &[[f64; 3]],
169 plane: &PlaneFrame,
170) -> bool {
171 if world_poly.len() < 2 {
172 return false;
173 }
174 let uv = project_polyline(plane, world_poly);
175 let link = classify_uv(&uv);
176 let new_uvs = link.point_uvs();
177
178 if let Some(pos) = refs.iter().position(|r| r.edge_name == edge_name) {
179 let structure_matches =
180 refs[pos].kind == link.kind() && refs[pos].point_ids.len() == new_uvs.len();
181 if structure_matches {
182 let mut moved = false;
184 let point_ids = refs[pos].point_ids.clone();
185 for (id, (u, v)) in point_ids.iter().zip(new_uvs.iter()) {
186 if let Some(p) = doc.point_mut(id) {
187 if (p.x - u).abs() > 1e-12 || (p.y - v).abs() > 1e-12 {
188 moved = true;
189 }
190 p.x = *u;
191 p.y = *v;
192 p.fixed = true;
193 p.construction = true;
194 p.external_reference = true;
195 }
196 }
197 if refs[pos].solid_name != solid_name {
198 refs[pos].solid_name = solid_name.to_string();
199 }
200 return moved;
201 }
202 remove_ref_entities(doc, &refs[pos].clone());
205 let (point_ids, geom_ids) = add_external_ref(doc, &link);
206 refs[pos] = ExternalRef {
207 edge_name: edge_name.to_string(),
208 solid_name: solid_name.to_string(),
209 point_ids,
210 geom_ids,
211 kind: link.kind().to_string(),
212 };
213 return true;
214 }
215
216 let (point_ids, geom_ids) = add_external_ref(doc, &link);
218 refs.push(ExternalRef {
219 edge_name: edge_name.to_string(),
220 solid_name: solid_name.to_string(),
221 point_ids,
222 geom_ids,
223 kind: link.kind().to_string(),
224 });
225 true
226}
227
228pub fn add_external_ref(doc: &mut SketchDoc, link: &EdgeLink) -> (Vec<Value>, Vec<Value>) {
232 let mut point_ids = Vec::new();
233 for (u, v) in link.point_uvs() {
234 let id = doc.next_point_id();
235 doc.points.push(SketchPoint {
236 id: id.clone(),
237 x: u,
238 y: v,
239 fixed: true,
240 construction: true,
241 external_reference: true,
242 });
243 push_ground(doc, &id);
244 point_ids.push(id);
245 }
246 let geom_ids = match link {
247 EdgeLink::Line { .. } => vec![push_construction_geometry(
248 doc,
249 "line",
250 vec![point_ids[0].clone(), point_ids[1].clone()],
251 )],
252 EdgeLink::Circle { .. } => vec![push_construction_geometry(
253 doc,
254 "circle",
255 vec![point_ids[0].clone(), point_ids[1].clone()],
256 )],
257 EdgeLink::Arc { .. } => vec![push_construction_geometry(
258 doc,
259 "arc",
260 vec![
261 point_ids[0].clone(),
262 point_ids[1].clone(),
263 point_ids[2].clone(),
264 ],
265 )],
266 EdgeLink::Polyline { .. } => point_ids
267 .windows(2)
268 .map(|w| push_construction_geometry(doc, "line", vec![w[0].clone(), w[1].clone()]))
269 .collect(),
270 };
271 (point_ids, geom_ids)
272}
273
274fn remove_ref_entities(doc: &mut SketchDoc, r: &ExternalRef) {
277 let pt_keys: HashSet<String> = r.point_ids.iter().map(id_key).collect();
278 let geo_keys: HashSet<String> = r.geom_ids.iter().map(id_key).collect();
279 doc.geometries.retain(|g| !geo_keys.contains(&id_key(&g.id)));
280 doc.points.retain(|p| !pt_keys.contains(&id_key(&p.id)));
281 doc.constraints
282 .retain(|c| !c.points().iter().any(|p| pt_keys.contains(&id_key(p))));
283}
284
285pub fn prune_dead_refs(doc: &SketchDoc, refs: &mut Vec<ExternalRef>) -> bool {
294 let before = refs.len();
295 refs.retain(|r| {
296 r.point_ids.iter().all(|id| doc.point(id).is_some())
297 && r.geom_ids.iter().all(|id| doc.geometry(id).is_some())
298 });
299 refs.len() != before
300}
301
302fn push_ground(doc: &mut SketchDoc, pid: &Value) {
305 let cid = doc.next_constraint_id();
306 let mut raw = Map::new();
307 raw.insert("id".to_string(), cid);
308 raw.insert("type".to_string(), Value::String("⏚".to_string()));
309 raw.insert("points".to_string(), Value::Array(vec![pid.clone()]));
310 doc.constraints.push(SketchConstraint { raw });
311}
312
313fn push_construction_geometry(doc: &mut SketchDoc, geom_type: &str, points: Vec<Value>) -> Value {
316 let id = doc.next_geometry_id();
317 let mut extra = Map::new();
318 extra.insert("construction".to_string(), Value::Bool(true));
319 doc.geometries.push(SketchGeometry {
320 id: id.clone(),
321 geom_type: geom_type.to_string(),
322 points,
323 extra,
324 });
325 id
326}
327
328fn polyline_extent(uv: &[(f64, f64)]) -> f64 {
332 let (mut minx, mut miny) = (f64::INFINITY, f64::INFINITY);
333 let (mut maxx, mut maxy) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
334 for &(x, y) in uv {
335 minx = minx.min(x);
336 miny = miny.min(y);
337 maxx = maxx.max(x);
338 maxy = maxy.max(y);
339 }
340 ((maxx - minx).powi(2) + (maxy - miny).powi(2)).sqrt()
341}
342
343fn fit_circle(p1: (f64, f64), p2: (f64, f64), p3: (f64, f64)) -> Option<(f64, f64, f64)> {
346 let (ax, ay) = p1;
347 let (bx, by) = p2;
348 let (cx, cy) = p3;
349 let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
351 if d.abs() < 1e-12 {
352 return None;
353 }
354 let a2 = ax * ax + ay * ay;
355 let b2 = bx * bx + by * by;
356 let c2 = cx * cx + cy * cy;
357 let ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d;
358 let uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d;
359 let r = dist((ux, uy), p1);
360 if !ux.is_finite() || !uy.is_finite() || !r.is_finite() {
361 return None;
362 }
363 Some((ux, uy, r))
364}
365
366