1use std::cmp::Ordering;
13use std::collections::HashSet;
14use std::f64::consts::PI;
15
16use serde_json::{Map, Value};
17
18use super::doc::{id_key, SketchConstraint, SketchDoc, SketchGeometry, SketchPoint};
19
20const CIRCLE_SAMPLES: usize = 96;
23const BEZIER_SEG_SAMPLES: usize = 24;
25const SNAP_EPS: f64 = 1e-6;
27
28#[derive(Clone, Copy, Debug)]
35struct Sample {
36 x: f64,
37 y: f64,
38 param: f64,
39}
40
41struct Sampled {
45 closed: bool,
46 max_param: f64,
47 seg_count: usize,
48 samples: Vec<Sample>,
49}
50
51fn sample_geometry(geo: &SketchGeometry, doc: &SketchDoc) -> Option<Sampled> {
55 let ids = &geo.points;
56 match geo.geom_type.as_str() {
57 "line" if ids.len() >= 2 => {
58 let p0 = doc.point(&ids[0])?;
59 let p1 = doc.point(&ids[1])?;
60 Some(Sampled {
61 closed: false,
62 max_param: 1.0,
63 seg_count: 1,
64 samples: vec![
65 Sample { x: p0.x, y: p0.y, param: 0.0 },
66 Sample { x: p1.x, y: p1.y, param: 1.0 },
67 ],
68 })
69 }
70 "circle" if ids.len() >= 2 => {
71 let pc = doc.point(&ids[0])?;
72 let pr = doc.point(&ids[1])?;
73 let r = (pr.x - pc.x).hypot(pr.y - pc.y);
74 if !r.is_finite() || r < 1e-9 {
75 return None;
76 }
77 let mut samples = Vec::with_capacity(CIRCLE_SAMPLES + 1);
78 for i in 0..=CIRCLE_SAMPLES {
79 let t = i as f64 / CIRCLE_SAMPLES as f64;
80 let a = t * 2.0 * PI;
81 samples.push(Sample {
82 x: pc.x + r * a.cos(),
83 y: pc.y + r * a.sin(),
84 param: t,
85 });
86 }
87 Some(Sampled { closed: true, max_param: 1.0, seg_count: 1, samples })
88 }
89 "arc" if ids.len() >= 3 => {
90 let pc = doc.point(&ids[0])?;
91 let pa = doc.point(&ids[1])?;
92 let pb = doc.point(&ids[2])?;
93 let r = (pa.x - pc.x).hypot(pa.y - pc.y);
94 if !r.is_finite() || r < 1e-9 {
95 return None;
96 }
97 let a0 = (pa.y - pc.y).atan2(pa.x - pc.x);
98 let a1 = (pb.y - pc.y).atan2(pb.x - pc.x);
99 let mut d = (a1 - a0).rem_euclid(2.0 * PI);
100 let full = d < 1e-6;
101 if full {
102 d = 2.0 * PI;
103 }
104 let segs = ((CIRCLE_SAMPLES as f64 * d / (2.0 * PI)).ceil() as usize).max(8);
105 let mut samples = Vec::with_capacity(segs + 1);
106 for i in 0..=segs {
107 let t = i as f64 / segs as f64;
108 let a = a0 + d * t;
109 samples.push(Sample {
110 x: pc.x + r * a.cos(),
111 y: pc.y + r * a.sin(),
112 param: t,
113 });
114 }
115 Some(Sampled { closed: full, max_param: 1.0, seg_count: 1, samples })
116 }
117 "bezier" if ids.len() >= 4 => {
118 let seg_count = (ids.len() - 1) / 3;
119 if seg_count < 1 {
120 return None;
121 }
122 let mut samples = Vec::new();
123 for seg in 0..seg_count {
124 let i0 = seg * 3;
125 let p0 = doc.point(&ids[i0])?;
126 let p1 = doc.point(&ids[i0 + 1])?;
127 let p2 = doc.point(&ids[i0 + 2])?;
128 let p3 = doc.point(&ids[i0 + 3])?;
129 for i in 0..=BEZIER_SEG_SAMPLES {
130 if seg > 0 && i == 0 {
131 continue; }
133 let t = i as f64 / BEZIER_SEG_SAMPLES as f64;
134 let mt = 1.0 - t;
135 let bx = mt * mt * mt * p0.x
136 + 3.0 * mt * mt * t * p1.x
137 + 3.0 * mt * t * t * p2.x
138 + t * t * t * p3.x;
139 let by = mt * mt * mt * p0.y
140 + 3.0 * mt * mt * t * p1.y
141 + 3.0 * mt * t * t * p2.y
142 + t * t * t * p3.y;
143 samples.push(Sample { x: bx, y: by, param: seg as f64 + t });
144 }
145 }
146 Some(Sampled {
147 closed: false,
148 max_param: seg_count as f64,
149 seg_count,
150 samples,
151 })
152 }
153 _ => None,
154 }
155}
156
157fn closest_param_on_samples(px: f64, py: f64, samples: &[Sample]) -> Option<f64> {
160 closest_point_on_samples(px, py, samples).map(|(param, _)| param)
161}
162
163fn closest_point_on_samples(px: f64, py: f64, samples: &[Sample]) -> Option<(f64, f64)> {
165 if samples.len() < 2 {
166 return None;
167 }
168 let mut best_param = samples[0].param;
169 let mut best_dist = f64::INFINITY;
170 for w in samples.windows(2) {
171 let (a, b) = (w[0], w[1]);
172 let vx = b.x - a.x;
173 let vy = b.y - a.y;
174 let l2 = (vx * vx + vy * vy).max(1e-12);
175 let t = (((px - a.x) * vx + (py - a.y) * vy) / l2).clamp(0.0, 1.0);
176 let nx = a.x + vx * t;
177 let ny = a.y + vy * t;
178 let d = (px - nx).hypot(py - ny);
179 if d < best_dist {
180 best_dist = d;
181 best_param = a.param + (b.param - a.param) * t;
182 }
183 }
184 Some((best_param, best_dist))
185}
186
187fn sample_tol(samples: &[Sample]) -> f64 {
190 if samples.is_empty() {
191 return 1e-3;
192 }
193 let (mut min_x, mut min_y, mut max_x, mut max_y) =
194 (f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
195 for s in samples {
196 min_x = min_x.min(s.x);
197 min_y = min_y.min(s.y);
198 max_x = max_x.max(s.x);
199 max_y = max_y.max(s.y);
200 }
201 let diag = (max_x - min_x).hypot(max_y - min_y);
202 if !diag.is_finite() || diag < 1e-9 {
203 return 1e-3;
204 }
205 (diag * 1e-3).clamp(1e-5, 1e-2)
206}
207
208#[derive(Clone, Copy, Debug)]
215struct SegHit {
216 x: f64,
217 y: f64,
218 ta: f64,
219 tb: f64,
220}
221
222fn segment_intersection(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2], eps: f64) -> Option<SegHit> {
227 let rdx = b[0] - a[0];
228 let rdy = b[1] - a[1];
229 let sdx = d[0] - c[0];
230 let sdy = d[1] - c[1];
231 let denom = rdx * sdy - rdy * sdx;
232 if denom.abs() < eps {
233 return None;
234 }
235 let t = ((c[0] - a[0]) * sdy - (c[1] - a[1]) * sdx) / denom;
236 let u = ((c[0] - a[0]) * rdy - (c[1] - a[1]) * rdx) / denom;
237 if t < -eps || t > 1.0 + eps || u < -eps || u > 1.0 + eps {
238 return None;
239 }
240 let tt = t.clamp(0.0, 1.0);
241 Some(SegHit {
242 x: a[0] + rdx * tt,
243 y: a[1] + rdy * tt,
244 ta: tt,
245 tb: u.clamp(0.0, 1.0),
246 })
247}
248
249#[derive(Clone, Debug)]
253struct Intersection {
254 param: f64,
255 x: f64,
256 y: f64,
257 other_param: Option<f64>,
258 other_geo_id: Value,
259 endpoint_snap: bool,
260}
261
262fn collect_intersections(
267 target: &Sampled,
268 other: &Sampled,
269 other_geo: &SketchGeometry,
270 out: &mut Vec<Intersection>,
271) {
272 let a = &target.samples;
273 let b = &other.samples;
274 if a.len() < 2 || b.len() < 2 {
275 return;
276 }
277 for i in 0..a.len() - 1 {
278 let (a0, a1) = (a[i], a[i + 1]);
279 for j in 0..b.len() - 1 {
280 let (b0, b1) = (b[j], b[j + 1]);
281 if let Some(hit) =
282 segment_intersection([a0.x, a0.y], [a1.x, a1.y], [b0.x, b0.y], [b1.x, b1.y], 1e-9)
283 {
284 out.push(Intersection {
285 param: a0.param + (a1.param - a0.param) * hit.ta,
286 x: hit.x,
287 y: hit.y,
288 other_param: Some(b0.param + (b1.param - b0.param) * hit.tb),
289 other_geo_id: other_geo.id.clone(),
290 endpoint_snap: false,
291 });
292 }
293 }
294 }
295 if !other.closed {
299 let tol = sample_tol(&target.samples);
300 for end in [b.first(), b.last()].into_iter().flatten() {
301 if let Some((param, dist)) = closest_point_on_samples(end.x, end.y, &target.samples) {
302 if param.is_finite() && dist.is_finite() && dist <= tol {
303 out.push(Intersection {
304 param,
305 x: end.x,
306 y: end.y,
307 other_param: Some(end.param),
308 other_geo_id: other_geo.id.clone(),
309 endpoint_snap: true,
310 });
311 }
312 }
313 }
314 }
315}
316
317struct Bounds {
325 prev: Option<Intersection>,
326 next: Option<Intersection>,
327 max_param: f64,
328}
329
330fn select_trim_bounds(intersections: &[Intersection], click_param: f64, target: &Sampled) -> Option<Bounds> {
337 let max_param = if target.max_param > 0.0 { target.max_param } else { 1.0 };
338 let param_eps = (1e-5f64).max(max_param * 1e-4);
339
340 let mut cleaned: Vec<Intersection> = Vec::new();
341 for inter in intersections {
342 if !inter.param.is_finite() {
343 continue;
344 }
345 let mut p = inter.param;
346 if target.closed {
347 p = p.rem_euclid(max_param);
348 if p < param_eps || p > max_param - param_eps {
349 p = 0.0;
350 }
351 } else if p <= param_eps || p >= max_param - param_eps {
352 continue;
353 }
354 let mut c = inter.clone();
355 c.param = p;
356 cleaned.push(c);
357 }
358 cleaned.sort_by(|a, b| a.param.partial_cmp(&b.param).unwrap_or(Ordering::Equal));
359
360 let mut uniq: Vec<Intersection> = Vec::new();
363 for inter in cleaned {
364 match uniq.last() {
365 Some(prev) if (inter.param - prev.param).abs() <= param_eps => {
366 if !prev.endpoint_snap && inter.endpoint_snap {
367 *uniq.last_mut().unwrap() = inter;
368 }
369 }
370 _ => uniq.push(inter),
371 }
372 }
373
374 if target.closed {
375 if uniq.len() < 2 {
376 return None;
377 }
378 let mut prev = None;
379 let mut next = None;
380 let mut best_next = f64::INFINITY;
381 let mut best_prev = f64::NEG_INFINITY;
382 for inter in &uniq {
383 let delta = (inter.param - click_param).rem_euclid(max_param);
384 if delta < param_eps {
385 continue;
386 }
387 if delta < best_next {
388 best_next = delta;
389 next = Some(inter.clone());
390 }
391 if delta > best_prev {
392 best_prev = delta;
393 prev = Some(inter.clone());
394 }
395 }
396 match (prev, next) {
397 (Some(prev), Some(next)) => Some(Bounds { prev: Some(prev), next: Some(next), max_param }),
398 _ => None,
399 }
400 } else {
401 if uniq.is_empty() {
402 return None;
403 }
404 let mut prev = None;
405 let mut next = None;
406 for inter in &uniq {
407 if inter.param < click_param - param_eps {
408 prev = Some(inter.clone());
409 } else if inter.param > click_param + param_eps {
410 next = Some(inter.clone());
411 break;
412 }
413 }
414 if prev.is_none() && next.is_none() {
415 return None;
416 }
417 Some(Bounds { prev, next, max_param })
418 }
419}
420
421fn get_or_create_point(doc: &mut SketchDoc, x: f64, y: f64) -> Value {
428 doc.snap_or_add_point(x, y, SNAP_EPS)
429}
430
431fn create_point(doc: &mut SketchDoc, x: f64, y: f64) -> Value {
434 let id = doc.next_point_id();
435 doc.points.push(SketchPoint {
436 id: id.clone(),
437 x,
438 y,
439 fixed: false,
440 construction: false,
441 external_reference: false,
442 });
443 id
444}
445
446fn add_geometry(doc: &mut SketchDoc, geom_type: &str, points: Vec<Value>, construction: bool) -> Value {
449 let id = doc.next_geometry_id();
450 let mut extra = Map::new();
451 extra.insert("construction".to_string(), Value::Bool(construction));
452 doc.geometries.push(SketchGeometry {
453 id: id.clone(),
454 geom_type: geom_type.to_string(),
455 points,
456 extra,
457 });
458 id
459}
460
461fn remove_geometry(doc: &mut SketchDoc, id: &Value) -> bool {
463 let key = id_key(id);
464 let before = doc.geometries.len();
465 doc.geometries.retain(|g| id_key(&g.id) != key);
466 doc.geometries.len() != before
467}
468
469fn cleanup_orphan_points(doc: &mut SketchDoc) {
473 let mut referenced: HashSet<String> = HashSet::new();
474 for g in &doc.geometries {
475 for pid in &g.points {
476 referenced.insert(id_key(pid));
477 }
478 }
479 for c in &doc.constraints {
480 for pid in c.points() {
481 referenced.insert(id_key(pid));
482 }
483 }
484 doc.points.retain(|p| referenced.contains(&id_key(&p.id)));
485}
486
487fn constraint_matches(c: &SketchConstraint, ctype: &str, points: &[Value]) -> bool {
494 if c.ctype() != Some(ctype) {
495 return false;
496 }
497 let cp = c.points();
498 let k = |v: &Value| id_key(v);
499 match ctype {
500 "≡" => {
501 if points.len() < 2 || cp.len() < 2 {
502 return false;
503 }
504 let (a, b) = (k(&points[0]), k(&points[1]));
505 let (p0, p1) = (k(&cp[0]), k(&cp[1]));
506 (p0 == a && p1 == b) || (p0 == b && p1 == a)
507 }
508 "⏛" => {
509 if points.len() < 3 || cp.len() < 3 {
510 return false;
511 }
512 let (a, b, p) = (k(&points[0]), k(&points[1]), k(&points[2]));
513 let (p0, p1, p2) = (k(&cp[0]), k(&cp[1]), k(&cp[2]));
514 ((p0 == a && p1 == b) || (p0 == b && p1 == a)) && p2 == p
515 }
516 "⇌" => {
517 if points.len() < 4 || cp.len() < 4 {
518 return false;
519 }
520 let (a, b, c0, d) = (k(&points[0]), k(&points[1]), k(&points[2]), k(&points[3]));
521 let (p0, p1, p2, p3) = (k(&cp[0]), k(&cp[1]), k(&cp[2]), k(&cp[3]));
522 let same = |x0: &str, x1: &str, y0: &str, y1: &str| {
523 (x0 == y0 && x1 == y1) || (x0 == y1 && x1 == y0)
524 };
525 (same(&p0, &p1, &a, &b) && same(&p2, &p3, &c0, &d))
526 || (same(&p0, &p1, &c0, &d) && same(&p2, &p3, &a, &b))
527 }
528 _ => false,
529 }
530}
531
532pub(crate) fn add_constraint_if_missing(doc: &mut SketchDoc, ctype: &str, points: Vec<Value>) -> bool {
537 if doc.constraints.iter().any(|c| constraint_matches(c, ctype, &points)) {
538 return false;
539 }
540 let id = doc.next_constraint_id();
541 let mut raw = Map::new();
542 raw.insert("id".to_string(), id);
543 raw.insert("type".to_string(), Value::String(ctype.to_string()));
544 raw.insert("points".to_string(), Value::Array(points));
545 raw.insert("labelX".to_string(), Value::from(0));
546 raw.insert("labelY".to_string(), Value::from(0));
547 raw.insert("displayStyle".to_string(), Value::String(String::new()));
548 raw.insert("value".to_string(), Value::Null);
549 raw.insert("valueNeedsSetup".to_string(), Value::Bool(true));
550 doc.constraints.push(SketchConstraint { raw });
551 true
552}
553
554fn ensure_point_on_line(
558 doc: &mut SketchDoc,
559 line_geo: &SketchGeometry,
560 point_id: &Value,
561 inter: &Intersection,
562) -> bool {
563 let ids = &line_geo.points;
564 if ids.len() < 2 {
565 return false;
566 }
567 let a_id = ids[0].clone();
568 let b_id = ids[1].clone();
569 let (a, b, p) = match (doc.point(&a_id), doc.point(&b_id), doc.point(point_id)) {
570 (Some(a), Some(b), Some(p)) => ((a.x, a.y), (b.x, b.y), (p.x, p.y)),
571 _ => return false,
572 };
573 let len = {
574 let l = (b.0 - a.0).hypot(b.1 - a.1);
575 if l == 0.0 {
576 1.0
577 } else {
578 l
579 }
580 };
581 let eps_param = 1e-3;
582 let eps_dist = (len * 1e-3).clamp(1e-5, 1e-2);
583 let near_a = inter.other_param.map_or(false, |op| op <= eps_param)
584 || (p.0 - a.0).hypot(p.1 - a.1) <= eps_dist;
585 let near_b = inter.other_param.map_or(false, |op| op >= 1.0 - eps_param)
586 || (p.0 - b.0).hypot(p.1 - b.1) <= eps_dist;
587
588 if near_a && id_key(point_id) != id_key(&a_id) {
589 return add_constraint_if_missing(doc, "≡", vec![a_id, point_id.clone()]);
590 }
591 if near_b && id_key(point_id) != id_key(&b_id) {
592 return add_constraint_if_missing(doc, "≡", vec![b_id, point_id.clone()]);
593 }
594 if id_key(point_id) == id_key(&a_id) || id_key(point_id) == id_key(&b_id) {
595 return false;
596 }
597 add_constraint_if_missing(doc, "⏛", vec![a_id, b_id, point_id.clone()])
598}
599
600fn ensure_point_on_arc(doc: &mut SketchDoc, arc_geo: &SketchGeometry, point_id: &Value) -> bool {
603 let ids = &arc_geo.points;
604 if ids.len() < 2 {
605 return false;
606 }
607 let center = ids[0].clone();
608 let rim = ids[1].clone();
609 if ids.iter().any(|id| id_key(id) == id_key(point_id)) {
614 return false;
615 }
616 add_constraint_if_missing(doc, "⇌", vec![center.clone(), rim, center, point_id.clone()])
617}
618
619fn apply_trim_intersection_constraint(doc: &mut SketchDoc, point_id: &Value, inter: &Intersection) {
623 let other = match doc.geometry(&inter.other_geo_id) {
624 Some(g) => g.clone(),
625 None => return,
626 };
627 match other.geom_type.as_str() {
628 "line" => {
629 ensure_point_on_line(doc, &other, point_id, inter);
630 }
631 "arc" | "circle" => {
632 ensure_point_on_arc(doc, &other, point_id);
633 }
634 _ => {}
635 }
636}
637
638fn add_line_if_valid(doc: &mut SketchDoc, a_id: &Value, b_id: &Value, construction: bool) -> bool {
645 if id_key(a_id) == id_key(b_id) {
646 return false;
647 }
648 let ok = match (doc.point(a_id), doc.point(b_id)) {
649 (Some(a), Some(b)) => (a.x - b.x).hypot(a.y - b.y) >= 1e-7,
650 _ => false,
651 };
652 if !ok {
653 return false;
654 }
655 add_geometry(doc, "line", vec![a_id.clone(), b_id.clone()], construction);
656 true
657}
658
659fn add_arc_if_valid(
661 doc: &mut SketchDoc,
662 center: &Value,
663 start: &Value,
664 end: &Value,
665 construction: bool,
666) -> bool {
667 if id_key(start) == id_key(end) {
668 return false;
669 }
670 let ok = match (doc.point(start), doc.point(end)) {
671 (Some(a), Some(b)) => (a.x - b.x).hypot(a.y - b.y) >= 1e-7,
672 _ => false,
673 };
674 if !ok {
675 return false;
676 }
677 add_geometry(doc, "arc", vec![center.clone(), start.clone(), end.clone()], construction);
678 true
679}
680
681fn trim_line(doc: &mut SketchDoc, geo: &SketchGeometry, bounds: &Bounds) -> bool {
686 let ids = &geo.points;
687 if ids.len() < 2 {
688 return false;
689 }
690 let id0 = ids[0].clone();
691 let id1 = ids[1].clone();
692 let (p0, p1) = match (doc.point(&id0), doc.point(&id1)) {
693 (Some(a), Some(b)) => ((a.x, a.y), (b.x, b.y)),
694 _ => return false,
695 };
696 let eps = 1e-5;
697 let use_prev = bounds.prev.as_ref().map_or(false, |b| b.param > eps);
698 let use_next = bounds.next.as_ref().map_or(false, |b| b.param < 1.0 - eps);
699 if !use_prev && !use_next {
700 return false;
701 }
702 if use_prev && use_next {
703 let pp = bounds.prev.as_ref().unwrap().param;
704 let np = bounds.next.as_ref().unwrap().param;
705 if np - pp <= eps {
706 return false;
707 }
708 }
709 let point_at = |t: f64| (p0.0 + (p1.0 - p0.0) * t, p0.1 + (p1.1 - p0.1) * t);
710 let construction = geo.construction();
711
712 let prev_id = if use_prev {
713 let (x, y) = point_at(bounds.prev.as_ref().unwrap().param);
714 get_or_create_point(doc, x, y)
715 } else {
716 id0.clone()
717 };
718 let next_id = if use_next {
719 let (x, y) = point_at(bounds.next.as_ref().unwrap().param);
720 get_or_create_point(doc, x, y)
721 } else {
722 id1.clone()
723 };
724
725 let mut added = false;
726 if use_prev {
727 added |= add_line_if_valid(doc, &id0, &prev_id, construction);
728 }
729 if use_next {
730 added |= add_line_if_valid(doc, &next_id, &id1, construction);
731 }
732 if use_prev {
733 apply_trim_intersection_constraint(doc, &prev_id, bounds.prev.as_ref().unwrap());
734 }
735 if use_next {
736 apply_trim_intersection_constraint(doc, &next_id, bounds.next.as_ref().unwrap());
737 }
738 if added {
739 remove_geometry(doc, &geo.id);
740 }
741 added
742}
743
744fn trim_arc(doc: &mut SketchDoc, geo: &SketchGeometry, bounds: &Bounds) -> bool {
747 let ids = &geo.points;
748 if ids.len() < 3 {
749 return false;
750 }
751 let id_c = ids[0].clone();
752 let id_a = ids[1].clone();
753 let id_b = ids[2].clone();
754 let (pc, pa) = match (doc.point(&id_c), doc.point(&id_a)) {
755 (Some(c), Some(a)) => ((c.x, c.y), (a.x, a.y)),
756 _ => return false,
757 };
758 let r = (pa.0 - pc.0).hypot(pa.1 - pc.1);
759 if !r.is_finite() || r < 1e-9 {
760 return false;
761 }
762 let eps = 1e-5;
763 let use_prev = bounds.prev.as_ref().map_or(false, |b| b.param > eps);
764 let use_next = bounds.next.as_ref().map_or(false, |b| b.param < 1.0 - eps);
765 if !use_prev && !use_next {
766 return false;
767 }
768 if use_prev && use_next {
769 let pp = bounds.prev.as_ref().unwrap().param;
770 let np = bounds.next.as_ref().unwrap().param;
771 if np - pp <= eps {
772 return false;
773 }
774 }
775 let on_circle = |inter: &Intersection| {
776 let ang = (inter.y - pc.1).atan2(inter.x - pc.0);
777 (pc.0 + r * ang.cos(), pc.1 + r * ang.sin())
778 };
779 let prev_id = if use_prev {
780 let (x, y) = on_circle(bounds.prev.as_ref().unwrap());
781 get_or_create_point(doc, x, y)
782 } else {
783 id_a.clone()
784 };
785 let next_id = if use_next {
786 let (x, y) = on_circle(bounds.next.as_ref().unwrap());
787 get_or_create_point(doc, x, y)
788 } else {
789 id_b.clone()
790 };
791 let construction = geo.construction();
792
793 let mut added = false;
794 if use_prev {
795 added |= add_arc_if_valid(doc, &id_c, &id_a, &prev_id, construction);
796 }
797 if use_next {
798 added |= add_arc_if_valid(doc, &id_c, &next_id, &id_b, construction);
799 }
800 if use_prev {
801 apply_trim_intersection_constraint(doc, &prev_id, bounds.prev.as_ref().unwrap());
802 }
803 if use_next {
804 apply_trim_intersection_constraint(doc, &next_id, bounds.next.as_ref().unwrap());
805 }
806 if added {
807 remove_geometry(doc, &geo.id);
808 }
809 added
810}
811
812fn trim_circle(doc: &mut SketchDoc, geo: &SketchGeometry, bounds: &Bounds) -> bool {
816 let ids = &geo.points;
817 if ids.len() < 2 {
818 return false;
819 }
820 let center = ids[0].clone();
821 let (pc, pr) = match (doc.point(¢er), doc.point(&ids[1])) {
822 (Some(c), Some(r)) => ((c.x, c.y), (r.x, r.y)),
823 _ => return false,
824 };
825 let r = (pr.0 - pc.0).hypot(pr.1 - pc.1);
826 if !r.is_finite() || r < 1e-9 {
827 return false;
828 }
829 let (prev, next) = match (&bounds.prev, &bounds.next) {
830 (Some(prev), Some(next)) => (prev, next),
831 _ => return false,
832 };
833 let max_param = if bounds.max_param > 0.0 { bounds.max_param } else { 1.0 };
834 let delta = (next.param - prev.param).rem_euclid(max_param);
835 if delta < 1e-5 || delta > max_param - 1e-5 {
836 return false;
837 }
838 let on_circle = |inter: &Intersection| {
839 let ang = (inter.y - pc.1).atan2(inter.x - pc.0);
840 (pc.0 + r * ang.cos(), pc.1 + r * ang.sin())
841 };
842 let (px, py) = on_circle(prev);
843 let prev_id = get_or_create_point(doc, px, py);
844 let (nx, ny) = on_circle(next);
845 let next_id = get_or_create_point(doc, nx, ny);
846 if id_key(&prev_id) == id_key(&next_id) {
847 return false;
848 }
849 let construction = geo.construction();
850 add_geometry(doc, "arc", vec![center, next_id.clone(), prev_id.clone()], construction);
851 apply_trim_intersection_constraint(doc, &prev_id, prev);
852 apply_trim_intersection_constraint(doc, &next_id, next);
853 remove_geometry(doc, &geo.id);
854 true
855}
856
857fn split_bezier_at(doc: &mut SketchDoc, geo_id: &Value, seg_index: usize, t: f64) -> Option<usize> {
861 let ids: Vec<Value> = doc.geometry(geo_id)?.points.clone();
862 let seg_count = ids.len().saturating_sub(1) / 3;
863 if seg_index >= seg_count {
864 return None;
865 }
866 let base = seg_index * 3;
867 let id0 = ids.get(base)?.clone();
868 let id1 = ids.get(base + 1)?.clone();
869 let id2 = ids.get(base + 2)?.clone();
870 let id3 = ids.get(base + 3)?.clone();
871 let (p0, p1, p2, p3) = {
872 let g0 = doc.point(&id0)?;
873 let g1 = doc.point(&id1)?;
874 let g2 = doc.point(&id2)?;
875 let g3 = doc.point(&id3)?;
876 ((g0.x, g0.y), (g1.x, g1.y), (g2.x, g2.y), (g3.x, g3.y))
877 };
878 let tt = t.clamp(0.0001, 0.9999);
879 let lerp = |a: (f64, f64), b: (f64, f64)| (a.0 + (b.0 - a.0) * tt, a.1 + (b.1 - a.1) * tt);
880 let q0 = lerp(p0, p1);
881 let q1 = lerp(p1, p2);
882 let q2 = lerp(p2, p3);
883 let r0 = lerp(q0, q1);
884 let r1 = lerp(q1, q2);
885 let s = lerp(r0, r1);
886
887 if let Some(pm) = doc.point_mut(&id1) {
888 pm.x = q0.0;
889 pm.y = q0.1;
890 }
891 if let Some(pm) = doc.point_mut(&id2) {
892 pm.x = q2.0;
893 pm.y = q2.1;
894 }
895 let r0_id = create_point(doc, r0.0, r0.1);
896 let s_id = create_point(doc, s.0, s.1);
897 let r1_id = create_point(doc, r1.0, r1.1);
898 let at = base + 2;
899 let g = doc.geometry_mut(geo_id)?;
900 g.points.splice(at..at, [r0_id, s_id, r1_id]);
901 Some(base + 3)
902}
903
904fn trim_bezier(doc: &mut SketchDoc, geo: &SketchGeometry, bounds: &Bounds, target: &Sampled) -> bool {
908 let geo_id = geo.id.clone();
909 let seg_count = if target.seg_count >= 1 {
910 target.seg_count
911 } else {
912 geo.points.len().saturating_sub(1) / 3
913 };
914 if seg_count < 1 {
915 return false;
916 }
917 let prev_int = bounds.prev.clone();
918 let next_int = bounds.next.clone();
919 if prev_int.is_none() && next_int.is_none() {
920 return false;
921 }
922 if let (Some(p), Some(n)) = (&prev_int, &next_int) {
923 if n.param - p.param <= 1e-5 {
924 return false;
925 }
926 }
927
928 struct Boundary {
931 kind: u8,
932 seg_index: usize,
933 t: f64,
934 pos: f64,
935 anchor_index: Option<usize>,
936 }
937 let mk = |kind: u8, param: f64| -> Boundary {
938 let seg_index = (param.floor() as isize).clamp(0, seg_count as isize - 1) as usize;
939 Boundary {
940 kind,
941 seg_index,
942 t: param - seg_index as f64,
943 pos: param,
944 anchor_index: None,
945 }
946 };
947 let mut boundaries: Vec<Boundary> = Vec::new();
948 if let Some(p) = &prev_int {
949 boundaries.push(mk(0, p.param));
950 }
951 if let Some(n) = &next_int {
952 boundaries.push(mk(1, n.param));
953 }
954 boundaries.sort_by(|a, b| a.pos.partial_cmp(&b.pos).unwrap_or(Ordering::Equal));
955
956 let mut splits_before = 0usize;
957 if boundaries.len() == 2 && boundaries[0].seg_index == boundaries[1].seg_index {
958 let first_t = boundaries[0].t;
959 let second_t = boundaries[1].t;
960 if second_t - first_t < 1e-5 {
961 return false;
962 }
963 let seg = boundaries[0].seg_index;
964 let res1 = match split_bezier_at(doc, &geo_id, seg + splits_before, first_t) {
965 Some(a) => a,
966 None => return false,
967 };
968 boundaries[0].anchor_index = Some(res1);
969 splits_before += 1;
970 let t2 = (second_t - first_t) / (1.0 - first_t);
971 let res2 = match split_bezier_at(doc, &geo_id, seg + splits_before, t2) {
972 Some(a) => a,
973 None => return false,
974 };
975 boundaries[1].anchor_index = Some(res2);
976 } else {
977 for b in boundaries.iter_mut() {
978 let res = match split_bezier_at(doc, &geo_id, b.seg_index + splits_before, b.t) {
979 Some(a) => a,
980 None => return false,
981 };
982 b.anchor_index = Some(res);
983 splits_before += 1;
984 }
985 }
986
987 let total_segs = match doc.geometry(&geo_id) {
988 Some(g) => g.points.len().saturating_sub(1) / 3,
989 None => return false,
990 };
991 let prev_seg = boundaries
992 .iter()
993 .find(|b| b.kind == 0)
994 .map(|b| b.anchor_index.unwrap_or(0) / 3)
995 .unwrap_or(0);
996 let next_seg = boundaries
997 .iter()
998 .find(|b| b.kind == 1)
999 .map(|b| b.anchor_index.unwrap_or(0) / 3)
1000 .unwrap_or(total_segs);
1001 if next_seg <= prev_seg {
1002 return false;
1003 }
1004 let mut keep_ranges: Vec<(usize, usize)> = Vec::new();
1005 if prev_seg > 0 {
1006 keep_ranges.push((0, prev_seg));
1007 }
1008 if next_seg < total_segs {
1009 keep_ranges.push((next_seg, total_segs));
1010 }
1011 let construction = geo.construction();
1012 let mut added = false;
1013 for (a, b) in keep_ranges {
1014 if b <= a {
1015 continue;
1016 }
1017 let start_idx = a * 3;
1018 let end_idx = b * 3;
1019 let plen = match doc.geometry(&geo_id) {
1020 Some(g) => g.points.len(),
1021 None => return false,
1022 };
1023 if end_idx >= plen {
1024 continue;
1025 }
1026 let pts: Vec<Value> = doc.geometry(&geo_id).unwrap().points[start_idx..=end_idx].to_vec();
1027 if pts.len() >= 4 {
1028 add_geometry(doc, "bezier", pts, construction);
1029 added = true;
1030 }
1031 }
1032 if added {
1033 remove_geometry(doc, &geo_id);
1034 }
1035 added
1036}
1037
1038pub fn pick_geometry_id(doc: &SketchDoc, u: f64, v: f64, radius: f64) -> Option<Value> {
1046 let mut best: Option<(f64, Value)> = None;
1047 for g in &doc.geometries {
1048 let poly = super::tessellate::geometry_polyline_uv(g, doc);
1049 if poly.len() < 2 {
1050 continue;
1051 }
1052 let mut dmin = f64::INFINITY;
1053 for seg in poly.windows(2) {
1054 let d = point_seg_dist(u, v, seg[0], seg[1]);
1055 if d < dmin {
1056 dmin = d;
1057 }
1058 }
1059 if dmin <= radius && best.as_ref().map_or(true, |(bd, _)| dmin < *bd) {
1060 best = Some((dmin, g.id.clone()));
1061 }
1062 }
1063 best.map(|(_, id)| id)
1064}
1065
1066pub fn trim_geometry(doc: &mut SketchDoc, geo_id: &Value, u: f64, v: f64) -> bool {
1073 let geo = match doc.geometry(geo_id) {
1074 Some(g) => g.clone(),
1075 None => return false,
1076 };
1077 let target = match sample_geometry(&geo, doc) {
1078 Some(t) if t.samples.len() >= 2 => t,
1079 _ => return false,
1080 };
1081 let click_param = match closest_param_on_samples(u, v, &target.samples) {
1082 Some(p) if p.is_finite() => p,
1083 _ => return false,
1084 };
1085
1086 let mut intersections: Vec<Intersection> = Vec::new();
1087 for other in &doc.geometries {
1088 if id_key(&other.id) == id_key(geo_id) {
1089 continue;
1090 }
1091 if let Some(sample) = sample_geometry(other, doc) {
1092 collect_intersections(&target, &sample, other, &mut intersections);
1093 }
1094 }
1095
1096 let changed = match select_trim_bounds(&intersections, click_param, &target) {
1097 None => remove_geometry(doc, geo_id),
1098 Some(bounds) => {
1099 let trimmed = match geo.geom_type.as_str() {
1100 "line" => trim_line(doc, &geo, &bounds),
1101 "circle" => trim_circle(doc, &geo, &bounds),
1102 "arc" => {
1103 if target.closed {
1104 trim_circle(doc, &geo, &bounds)
1105 } else {
1106 trim_arc(doc, &geo, &bounds)
1107 }
1108 }
1109 "bezier" => trim_bezier(doc, &geo, &bounds, &target),
1110 _ => false,
1111 };
1112 if trimmed {
1113 true
1114 } else {
1115 remove_geometry(doc, geo_id)
1116 }
1117 }
1118 };
1119 if changed {
1120 cleanup_orphan_points(doc);
1121 }
1122 changed
1123}
1124
1125fn point_seg_dist(px: f64, py: f64, a: [f64; 2], b: [f64; 2]) -> f64 {
1127 let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
1128 let len2 = dx * dx + dy * dy;
1129 let t = if len2 <= 1e-18 {
1130 0.0
1131 } else {
1132 (((px - a[0]) * dx + (py - a[1]) * dy) / len2).clamp(0.0, 1.0)
1133 };
1134 let (cx, cy) = (a[0] + t * dx, a[1] + t * dy);
1135 (px - cx).hypot(py - cy)
1136}
1137
1138