1use std::f64::consts::TAU;
2
3use indexmap::IndexSet;
4use kittycad_modeling_cmds::units::UnitLength;
5
6use crate::{
7 execution::types::adjust_length,
8 frontend::{
9 api::{Number, Object, ObjectId, ObjectKind},
10 sketch::{Constraint, Segment, SegmentCtor},
11 },
12 pretty::NumericSuffix,
13};
14
15#[cfg(test)]
16mod tests;
17
18const EPSILON_PARALLEL: f64 = 1e-10;
20const EPSILON_POINT_ON_SEGMENT: f64 = 1e-6;
21
22fn suffix_to_unit(suffix: NumericSuffix) -> UnitLength {
24 match suffix {
25 NumericSuffix::Mm => UnitLength::Millimeters,
26 NumericSuffix::Cm => UnitLength::Centimeters,
27 NumericSuffix::M => UnitLength::Meters,
28 NumericSuffix::Inch => UnitLength::Inches,
29 NumericSuffix::Ft => UnitLength::Feet,
30 NumericSuffix::Yd => UnitLength::Yards,
31 _ => UnitLength::Millimeters,
32 }
33}
34
35fn number_to_unit(n: &Number, target_unit: UnitLength) -> f64 {
37 adjust_length(suffix_to_unit(n.units), n.value, target_unit).0
38}
39
40fn unit_to_number(value: f64, source_unit: UnitLength, target_suffix: NumericSuffix) -> Number {
42 let (value, _) = adjust_length(source_unit, value, suffix_to_unit(target_suffix));
43 Number {
44 value,
45 units: target_suffix,
46 }
47}
48
49fn normalize_trim_points_to_unit(points: &[Coords2d], default_unit: UnitLength) -> Vec<Coords2d> {
51 points
52 .iter()
53 .map(|point| Coords2d {
54 x: adjust_length(UnitLength::Millimeters, point.x, default_unit).0,
55 y: adjust_length(UnitLength::Millimeters, point.y, default_unit).0,
56 })
57 .collect()
58}
59
60#[derive(Debug, Clone, Copy)]
62pub struct Coords2d {
63 pub x: f64,
64 pub y: f64,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum LineEndpoint {
70 Start,
71 End,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum ArcPoint {
77 Start,
78 End,
79 Center,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum TrimDirection {
85 Left,
86 Right,
87}
88
89#[derive(Debug, Clone)]
97pub enum TrimItem {
98 Spawn {
99 trim_spawn_seg_id: ObjectId,
100 trim_spawn_coords: Coords2d,
101 next_index: usize,
102 },
103 None {
104 next_index: usize,
105 },
106}
107
108#[derive(Debug, Clone)]
115pub enum TrimTermination {
116 SegEndPoint {
117 trim_termination_coords: Coords2d,
118 },
119 Intersection {
120 trim_termination_coords: Coords2d,
121 intersecting_seg_id: ObjectId,
122 },
123 TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
124 trim_termination_coords: Coords2d,
125 intersecting_seg_id: ObjectId,
126 other_segment_point_id: ObjectId,
127 },
128}
129
130#[derive(Debug, Clone)]
132pub struct TrimTerminations {
133 pub left_side: TrimTermination,
134 pub right_side: TrimTermination,
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum AttachToEndpoint {
140 Start,
141 End,
142 Segment,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum EndpointChanged {
148 Start,
149 End,
150}
151
152#[derive(Debug, Clone)]
154pub struct CoincidentData {
155 pub intersecting_seg_id: ObjectId,
156 pub intersecting_endpoint_point_id: Option<ObjectId>,
157 pub existing_point_segment_constraint_id: Option<ObjectId>,
158}
159
160#[derive(Debug, Clone)]
162pub struct ConstraintToMigrate {
163 pub constraint_id: ObjectId,
164 pub other_entity_id: ObjectId,
165 pub is_point_point: bool,
168 pub attach_to_endpoint: AttachToEndpoint,
169}
170
171#[derive(Debug, Clone)]
172#[allow(clippy::large_enum_variant)]
173pub enum TrimOperation {
174 SimpleTrim {
175 segment_to_trim_id: ObjectId,
176 },
177 EditSegment {
178 segment_id: ObjectId,
179 ctor: SegmentCtor,
180 endpoint_changed: EndpointChanged,
181 },
182 AddCoincidentConstraint {
183 segment_id: ObjectId,
184 endpoint_changed: EndpointChanged,
185 segment_or_point_to_make_coincident_to: ObjectId,
186 intersecting_endpoint_point_id: Option<ObjectId>,
187 },
188 SplitSegment {
189 segment_id: ObjectId,
190 left_trim_coords: Coords2d,
191 right_trim_coords: Coords2d,
192 original_end_coords: Coords2d,
193 left_side: Box<TrimTermination>,
194 right_side: Box<TrimTermination>,
195 left_side_coincident_data: CoincidentData,
196 right_side_coincident_data: CoincidentData,
197 constraints_to_migrate: Vec<ConstraintToMigrate>,
198 constraints_to_delete: Vec<ObjectId>,
199 },
200 DeleteConstraints {
201 constraint_ids: Vec<ObjectId>,
202 },
203}
204
205pub fn is_point_on_line_segment(
209 point: Coords2d,
210 segment_start: Coords2d,
211 segment_end: Coords2d,
212 epsilon: f64,
213) -> Option<Coords2d> {
214 let dx = segment_end.x - segment_start.x;
215 let dy = segment_end.y - segment_start.y;
216 let segment_length_sq = dx * dx + dy * dy;
217
218 if segment_length_sq < EPSILON_PARALLEL {
219 let dist_sq = (point.x - segment_start.x) * (point.x - segment_start.x)
221 + (point.y - segment_start.y) * (point.y - segment_start.y);
222 if dist_sq <= epsilon * epsilon {
223 return Some(point);
224 }
225 return None;
226 }
227
228 let point_dx = point.x - segment_start.x;
229 let point_dy = point.y - segment_start.y;
230 let projection_param = (point_dx * dx + point_dy * dy) / segment_length_sq;
231
232 if !(0.0..=1.0).contains(&projection_param) {
234 return None;
235 }
236
237 let projected_point = Coords2d {
239 x: segment_start.x + projection_param * dx,
240 y: segment_start.y + projection_param * dy,
241 };
242
243 let dist_dx = point.x - projected_point.x;
245 let dist_dy = point.y - projected_point.y;
246 let distance_sq = dist_dx * dist_dx + dist_dy * dist_dy;
247
248 if distance_sq <= epsilon * epsilon {
249 Some(point)
250 } else {
251 None
252 }
253}
254
255pub fn line_segment_intersection(
259 line1_start: Coords2d,
260 line1_end: Coords2d,
261 line2_start: Coords2d,
262 line2_end: Coords2d,
263 epsilon: f64,
264) -> Option<Coords2d> {
265 if let Some(point) = is_point_on_line_segment(line1_start, line2_start, line2_end, epsilon) {
267 return Some(point);
268 }
269
270 if let Some(point) = is_point_on_line_segment(line1_end, line2_start, line2_end, epsilon) {
271 return Some(point);
272 }
273
274 if let Some(point) = is_point_on_line_segment(line2_start, line1_start, line1_end, epsilon) {
275 return Some(point);
276 }
277
278 if let Some(point) = is_point_on_line_segment(line2_end, line1_start, line1_end, epsilon) {
279 return Some(point);
280 }
281
282 let x1 = line1_start.x;
284 let y1 = line1_start.y;
285 let x2 = line1_end.x;
286 let y2 = line1_end.y;
287 let x3 = line2_start.x;
288 let y3 = line2_start.y;
289 let x4 = line2_end.x;
290 let y4 = line2_end.y;
291
292 let denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
293 if denominator.abs() < EPSILON_PARALLEL {
294 return None;
296 }
297
298 let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denominator;
299 let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denominator;
300
301 if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
303 let x = x1 + t * (x2 - x1);
304 let y = y1 + t * (y2 - y1);
305 return Some(Coords2d { x, y });
306 }
307
308 None
309}
310
311pub fn project_point_onto_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
316 let dx = segment_end.x - segment_start.x;
317 let dy = segment_end.y - segment_start.y;
318 let segment_length_sq = dx * dx + dy * dy;
319
320 if segment_length_sq < EPSILON_PARALLEL {
321 return 0.0;
323 }
324
325 let point_dx = point.x - segment_start.x;
326 let point_dy = point.y - segment_start.y;
327
328 (point_dx * dx + point_dy * dy) / segment_length_sq
329}
330
331pub fn perpendicular_distance_to_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
335 let dx = segment_end.x - segment_start.x;
336 let dy = segment_end.y - segment_start.y;
337 let segment_length_sq = dx * dx + dy * dy;
338
339 if segment_length_sq < EPSILON_PARALLEL {
340 let dist_dx = point.x - segment_start.x;
342 let dist_dy = point.y - segment_start.y;
343 return (dist_dx * dist_dx + dist_dy * dist_dy).sqrt();
344 }
345
346 let point_dx = point.x - segment_start.x;
348 let point_dy = point.y - segment_start.y;
349
350 let t = (point_dx * dx + point_dy * dy) / segment_length_sq;
352
353 let clamped_t = t.clamp(0.0, 1.0);
355 let closest_point = Coords2d {
356 x: segment_start.x + clamped_t * dx,
357 y: segment_start.y + clamped_t * dy,
358 };
359
360 let dist_dx = point.x - closest_point.x;
362 let dist_dy = point.y - closest_point.y;
363 (dist_dx * dist_dx + dist_dy * dist_dy).sqrt()
364}
365
366pub fn is_point_on_arc(point: Coords2d, center: Coords2d, start: Coords2d, end: Coords2d, epsilon: f64) -> bool {
370 let radius = ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
372
373 let dist_from_center =
375 ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
376 if (dist_from_center - radius).abs() > epsilon {
377 return false;
378 }
379
380 let start_angle = libm::atan2(start.y - center.y, start.x - center.x);
382 let end_angle = libm::atan2(end.y - center.y, end.x - center.x);
383 let point_angle = libm::atan2(point.y - center.y, point.x - center.x);
384
385 let normalize_angle = |angle: f64| -> f64 {
387 if !angle.is_finite() {
388 return angle;
389 }
390 let mut normalized = angle;
391 while normalized < 0.0 {
392 normalized += TAU;
393 }
394 while normalized >= TAU {
395 normalized -= TAU;
396 }
397 normalized
398 };
399
400 let normalized_start = normalize_angle(start_angle);
401 let normalized_end = normalize_angle(end_angle);
402 let normalized_point = normalize_angle(point_angle);
403
404 if normalized_start < normalized_end {
408 normalized_point >= normalized_start && normalized_point <= normalized_end
410 } else {
411 normalized_point >= normalized_start || normalized_point <= normalized_end
413 }
414}
415
416pub fn line_arc_intersection(
420 line_start: Coords2d,
421 line_end: Coords2d,
422 arc_center: Coords2d,
423 arc_start: Coords2d,
424 arc_end: Coords2d,
425 epsilon: f64,
426) -> Option<Coords2d> {
427 let radius = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
429 + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
430 .sqrt();
431
432 let translated_line_start = Coords2d {
434 x: line_start.x - arc_center.x,
435 y: line_start.y - arc_center.y,
436 };
437 let translated_line_end = Coords2d {
438 x: line_end.x - arc_center.x,
439 y: line_end.y - arc_center.y,
440 };
441
442 let dx = translated_line_end.x - translated_line_start.x;
444 let dy = translated_line_end.y - translated_line_start.y;
445
446 let a = dx * dx + dy * dy;
453 let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
454 let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
455 - radius * radius;
456
457 let discriminant = b * b - 4.0 * a * c;
458
459 if discriminant < 0.0 {
460 return None;
462 }
463
464 if a.abs() < EPSILON_PARALLEL {
465 let dist_from_center = (translated_line_start.x * translated_line_start.x
467 + translated_line_start.y * translated_line_start.y)
468 .sqrt();
469 if (dist_from_center - radius).abs() <= epsilon {
470 let point = line_start;
472 if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
473 return Some(point);
474 }
475 }
476 return None;
477 }
478
479 let sqrt_discriminant = discriminant.sqrt();
480 let t1 = (-b - sqrt_discriminant) / (2.0 * a);
481 let t2 = (-b + sqrt_discriminant) / (2.0 * a);
482
483 let mut candidates: Vec<(f64, Coords2d)> = Vec::new();
485 if (0.0..=1.0).contains(&t1) {
486 let point = Coords2d {
487 x: line_start.x + t1 * (line_end.x - line_start.x),
488 y: line_start.y + t1 * (line_end.y - line_start.y),
489 };
490 candidates.push((t1, point));
491 }
492 if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
493 let point = Coords2d {
494 x: line_start.x + t2 * (line_end.x - line_start.x),
495 y: line_start.y + t2 * (line_end.y - line_start.y),
496 };
497 candidates.push((t2, point));
498 }
499
500 for (_t, point) in candidates {
502 if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
503 return Some(point);
504 }
505 }
506
507 None
508}
509
510pub fn project_point_onto_arc(point: Coords2d, arc_center: Coords2d, arc_start: Coords2d, arc_end: Coords2d) -> f64 {
513 let start_angle = libm::atan2(arc_start.y - arc_center.y, arc_start.x - arc_center.x);
515 let end_angle = libm::atan2(arc_end.y - arc_center.y, arc_end.x - arc_center.x);
516 let point_angle = libm::atan2(point.y - arc_center.y, point.x - arc_center.x);
517
518 let normalize_angle = |angle: f64| -> f64 {
520 if !angle.is_finite() {
521 return angle;
522 }
523 let mut normalized = angle;
524 while normalized < 0.0 {
525 normalized += TAU;
526 }
527 while normalized >= TAU {
528 normalized -= TAU;
529 }
530 normalized
531 };
532
533 let normalized_start = normalize_angle(start_angle);
534 let normalized_end = normalize_angle(end_angle);
535 let normalized_point = normalize_angle(point_angle);
536
537 let arc_length = if normalized_start < normalized_end {
539 normalized_end - normalized_start
540 } else {
541 TAU - normalized_start + normalized_end
543 };
544
545 if arc_length < EPSILON_PARALLEL {
546 return 0.0;
548 }
549
550 let point_arc_length = if normalized_start < normalized_end {
552 if normalized_point >= normalized_start && normalized_point <= normalized_end {
553 normalized_point - normalized_start
554 } else {
555 let dist_to_start = (normalized_point - normalized_start)
557 .abs()
558 .min(TAU - (normalized_point - normalized_start).abs());
559 let dist_to_end = (normalized_point - normalized_end)
560 .abs()
561 .min(TAU - (normalized_point - normalized_end).abs());
562 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
563 }
564 } else {
565 if normalized_point >= normalized_start || normalized_point <= normalized_end {
567 if normalized_point >= normalized_start {
568 normalized_point - normalized_start
569 } else {
570 TAU - normalized_start + normalized_point
571 }
572 } else {
573 let dist_to_start = (normalized_point - normalized_start)
575 .abs()
576 .min(TAU - (normalized_point - normalized_start).abs());
577 let dist_to_end = (normalized_point - normalized_end)
578 .abs()
579 .min(TAU - (normalized_point - normalized_end).abs());
580 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
581 }
582 };
583
584 point_arc_length / arc_length
586}
587
588pub fn arc_arc_intersection(
590 arc1_center: Coords2d,
591 arc1_start: Coords2d,
592 arc1_end: Coords2d,
593 arc2_center: Coords2d,
594 arc2_start: Coords2d,
595 arc2_end: Coords2d,
596 epsilon: f64,
597) -> Option<Coords2d> {
598 let r1 = ((arc1_start.x - arc1_center.x) * (arc1_start.x - arc1_center.x)
600 + (arc1_start.y - arc1_center.y) * (arc1_start.y - arc1_center.y))
601 .sqrt();
602 let r2 = ((arc2_start.x - arc2_center.x) * (arc2_start.x - arc2_center.x)
603 + (arc2_start.y - arc2_center.y) * (arc2_start.y - arc2_center.y))
604 .sqrt();
605
606 let dx = arc2_center.x - arc1_center.x;
608 let dy = arc2_center.y - arc1_center.y;
609 let d = (dx * dx + dy * dy).sqrt();
610
611 if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon {
613 return None;
615 }
616
617 if d < EPSILON_PARALLEL {
619 return None;
621 }
622
623 let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
626 let h_sq = r1 * r1 - a * a;
627
628 if h_sq < 0.0 {
630 return None;
631 }
632
633 let h = h_sq.sqrt();
634
635 if h.is_nan() {
637 return None;
638 }
639
640 let ux = dx / d;
642 let uy = dy / d;
643
644 let px = -uy;
646 let py = ux;
647
648 let mid_point = Coords2d {
650 x: arc1_center.x + a * ux,
651 y: arc1_center.y + a * uy,
652 };
653
654 let intersection1 = Coords2d {
656 x: mid_point.x + h * px,
657 y: mid_point.y + h * py,
658 };
659 let intersection2 = Coords2d {
660 x: mid_point.x - h * px,
661 y: mid_point.y - h * py,
662 };
663
664 let mut candidates: Vec<Coords2d> = Vec::new();
666
667 if is_point_on_arc(intersection1, arc1_center, arc1_start, arc1_end, epsilon)
668 && is_point_on_arc(intersection1, arc2_center, arc2_start, arc2_end, epsilon)
669 {
670 candidates.push(intersection1);
671 }
672
673 if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
674 if is_point_on_arc(intersection2, arc1_center, arc1_start, arc1_end, epsilon)
676 && is_point_on_arc(intersection2, arc2_center, arc2_start, arc2_end, epsilon)
677 {
678 candidates.push(intersection2);
679 }
680 }
681
682 candidates.first().copied()
684}
685
686fn get_point_coords_from_native(objects: &[Object], point_id: ObjectId, default_unit: UnitLength) -> Option<Coords2d> {
689 let point_obj = objects.get(point_id.0)?;
690
691 let ObjectKind::Segment { segment } = &point_obj.kind else {
693 return None;
694 };
695
696 let Segment::Point(point) = segment else {
697 return None;
698 };
699
700 Some(Coords2d {
702 x: number_to_unit(&point.position.x, default_unit),
703 y: number_to_unit(&point.position.y, default_unit),
704 })
705}
706
707pub fn get_position_coords_for_line(
710 segment_obj: &Object,
711 which: LineEndpoint,
712 objects: &[Object],
713 default_unit: UnitLength,
714) -> Option<Coords2d> {
715 let ObjectKind::Segment { segment } = &segment_obj.kind else {
716 return None;
717 };
718
719 let Segment::Line(line) = segment else {
720 return None;
721 };
722
723 let point_id = match which {
725 LineEndpoint::Start => line.start,
726 LineEndpoint::End => line.end,
727 };
728
729 get_point_coords_from_native(objects, point_id, default_unit)
730}
731
732fn is_point_coincident_with_segment_native(point_id: ObjectId, segment_id: ObjectId, objects: &[Object]) -> bool {
734 for obj in objects {
736 let ObjectKind::Constraint { constraint } = &obj.kind else {
737 continue;
738 };
739
740 let Constraint::Coincident(coincident) = constraint else {
741 continue;
742 };
743
744 let has_point = coincident.segments.contains(&point_id);
746 let has_segment = coincident.segments.contains(&segment_id);
747
748 if has_point && has_segment {
749 return true;
750 }
751 }
752 false
753}
754
755pub fn get_position_coords_from_arc(
757 segment_obj: &Object,
758 which: ArcPoint,
759 objects: &[Object],
760 default_unit: UnitLength,
761) -> Option<Coords2d> {
762 let ObjectKind::Segment { segment } = &segment_obj.kind else {
763 return None;
764 };
765
766 let Segment::Arc(arc) = segment else {
767 return None;
768 };
769
770 let point_id = match which {
772 ArcPoint::Start => arc.start,
773 ArcPoint::End => arc.end,
774 ArcPoint::Center => arc.center,
775 };
776
777 get_point_coords_from_native(objects, point_id, default_unit)
778}
779
780pub fn get_next_trim_spawn(
808 points: &[Coords2d],
809 start_index: usize,
810 objects: &[Object],
811 default_unit: UnitLength,
812) -> TrimItem {
813 for i in start_index..points.len().saturating_sub(1) {
815 let p1 = points[i];
816 let p2 = points[i + 1];
817
818 for obj in objects.iter() {
820 let ObjectKind::Segment { segment } = &obj.kind else {
822 continue;
823 };
824
825 if let Segment::Line(_line) = segment {
827 let start_point = get_position_coords_for_line(obj, LineEndpoint::Start, objects, default_unit);
828 let end_point = get_position_coords_for_line(obj, LineEndpoint::End, objects, default_unit);
829
830 if let (Some(start), Some(end)) = (start_point, end_point)
831 && let Some(intersection) = line_segment_intersection(p1, p2, start, end, EPSILON_POINT_ON_SEGMENT)
832 {
833 let seg_id = obj.id;
835
836 return TrimItem::Spawn {
837 trim_spawn_seg_id: seg_id,
838 trim_spawn_coords: intersection,
839 next_index: i, };
841 }
842 }
843
844 if let Segment::Arc(_arc) = segment {
846 let center_point = get_position_coords_from_arc(obj, ArcPoint::Center, objects, default_unit);
847 let start_point = get_position_coords_from_arc(obj, ArcPoint::Start, objects, default_unit);
848 let end_point = get_position_coords_from_arc(obj, ArcPoint::End, objects, default_unit);
849
850 if let (Some(center), Some(start), Some(end)) = (center_point, start_point, end_point)
851 && let Some(intersection) =
852 line_arc_intersection(p1, p2, center, start, end, EPSILON_POINT_ON_SEGMENT)
853 {
854 let seg_id = obj.id;
856
857 return TrimItem::Spawn {
858 trim_spawn_seg_id: seg_id,
859 trim_spawn_coords: intersection,
860 next_index: i, };
862 }
863 }
864 }
865 }
866
867 TrimItem::None {
869 next_index: points.len().saturating_sub(1),
870 }
871}
872
873pub fn get_trim_spawn_terminations(
928 trim_spawn_seg_id: ObjectId,
929 trim_spawn_coords: &[Coords2d],
930 objects: &[Object],
931 default_unit: UnitLength,
932) -> Result<TrimTerminations, String> {
933 let trim_spawn_seg = objects.iter().find(|obj| obj.id == trim_spawn_seg_id);
935
936 let trim_spawn_seg = match trim_spawn_seg {
937 Some(seg) => seg,
938 None => {
939 return Err(format!("Trim spawn segment {} not found", trim_spawn_seg_id.0));
940 }
941 };
942
943 let ObjectKind::Segment { segment } = &trim_spawn_seg.kind else {
945 return Err(format!("Trim spawn segment {} is not a segment", trim_spawn_seg_id.0));
946 };
947
948 let (segment_start, segment_end, segment_center) = match segment {
949 Segment::Line(_) => {
950 let start = get_position_coords_for_line(trim_spawn_seg, LineEndpoint::Start, objects, default_unit)
951 .ok_or_else(|| {
952 format!(
953 "Could not get start coordinates for line segment {}",
954 trim_spawn_seg_id.0
955 )
956 })?;
957 let end = get_position_coords_for_line(trim_spawn_seg, LineEndpoint::End, objects, default_unit)
958 .ok_or_else(|| format!("Could not get end coordinates for line segment {}", trim_spawn_seg_id.0))?;
959 (start, end, None)
960 }
961 Segment::Arc(_) => {
962 let start = get_position_coords_from_arc(trim_spawn_seg, ArcPoint::Start, objects, default_unit)
963 .ok_or_else(|| {
964 format!(
965 "Could not get start coordinates for arc segment {}",
966 trim_spawn_seg_id.0
967 )
968 })?;
969 let end = get_position_coords_from_arc(trim_spawn_seg, ArcPoint::End, objects, default_unit)
970 .ok_or_else(|| format!("Could not get end coordinates for arc segment {}", trim_spawn_seg_id.0))?;
971 let center = get_position_coords_from_arc(trim_spawn_seg, ArcPoint::Center, objects, default_unit)
972 .ok_or_else(|| {
973 format!(
974 "Could not get center coordinates for arc segment {}",
975 trim_spawn_seg_id.0
976 )
977 })?;
978 (start, end, Some(center))
979 }
980 _ => {
981 return Err(format!(
982 "Trim spawn segment {} is not a Line or Arc",
983 trim_spawn_seg_id.0
984 ));
985 }
986 };
987
988 let mut all_intersections: Vec<(Coords2d, usize)> = Vec::new();
993
994 for i in 0..trim_spawn_coords.len().saturating_sub(1) {
995 let p1 = trim_spawn_coords[i];
996 let p2 = trim_spawn_coords[i + 1];
997
998 match segment {
999 Segment::Line(_) => {
1000 if let Some(intersection) =
1001 line_segment_intersection(p1, p2, segment_start, segment_end, EPSILON_POINT_ON_SEGMENT)
1002 {
1003 all_intersections.push((intersection, i));
1004 }
1005 }
1006 Segment::Arc(_) => {
1007 if let Some(center) = segment_center
1008 && let Some(intersection) =
1009 line_arc_intersection(p1, p2, center, segment_start, segment_end, EPSILON_POINT_ON_SEGMENT)
1010 {
1011 all_intersections.push((intersection, i));
1012 }
1013 }
1014 Segment::Point(_) | Segment::Circle(_) => {
1015 }
1017 }
1018 }
1019
1020 let intersection_point = if all_intersections.is_empty() {
1023 return Err("Could not find intersection point between polyline and trim spawn segment".to_string());
1024 } else {
1025 let mid_index = (trim_spawn_coords.len() - 1) / 2;
1027 let mid_point = trim_spawn_coords[mid_index];
1028
1029 let mut min_dist = f64::INFINITY;
1031 let mut closest_intersection = all_intersections[0].0;
1032
1033 for (intersection, _) in &all_intersections {
1034 let dist = ((intersection.x - mid_point.x) * (intersection.x - mid_point.x)
1035 + (intersection.y - mid_point.y) * (intersection.y - mid_point.y))
1036 .sqrt();
1037 if dist < min_dist {
1038 min_dist = dist;
1039 closest_intersection = *intersection;
1040 }
1041 }
1042
1043 closest_intersection
1044 };
1045
1046 let intersection_t = match segment {
1048 Segment::Line(_) => project_point_onto_segment(intersection_point, segment_start, segment_end),
1049 Segment::Arc(_) => {
1050 if let Some(center) = segment_center {
1051 project_point_onto_arc(intersection_point, center, segment_start, segment_end)
1052 } else {
1053 return Err("Arc segment missing center".to_string());
1054 }
1055 }
1056 _ => {
1057 return Err("Invalid segment type for trim spawn".to_string());
1058 }
1059 };
1060
1061 let left_termination = find_termination_in_direction(
1063 trim_spawn_seg,
1064 intersection_point,
1065 intersection_t,
1066 TrimDirection::Left,
1067 objects,
1068 SegmentGeometry {
1069 start: segment_start,
1070 end: segment_end,
1071 center: segment_center,
1072 },
1073 default_unit,
1074 )?;
1075
1076 let right_termination = find_termination_in_direction(
1077 trim_spawn_seg,
1078 intersection_point,
1079 intersection_t,
1080 TrimDirection::Right,
1081 objects,
1082 SegmentGeometry {
1083 start: segment_start,
1084 end: segment_end,
1085 center: segment_center,
1086 },
1087 default_unit,
1088 )?;
1089
1090 Ok(TrimTerminations {
1091 left_side: left_termination,
1092 right_side: right_termination,
1093 })
1094}
1095
1096#[derive(Debug, Clone, Copy)]
1098struct SegmentGeometry {
1099 start: Coords2d,
1100 end: Coords2d,
1101 center: Option<Coords2d>,
1102}
1103
1104fn find_termination_in_direction(
1157 trim_spawn_seg: &Object,
1158 _intersection_point: Coords2d,
1159 intersection_t: f64,
1160 direction: TrimDirection,
1161 objects: &[Object],
1162 segment_geometry: SegmentGeometry,
1163 default_unit: UnitLength,
1164) -> Result<TrimTermination, String> {
1165 let ObjectKind::Segment { segment } = &trim_spawn_seg.kind else {
1167 return Err("Trim spawn segment is not a segment".to_string());
1168 };
1169
1170 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1172 enum CandidateType {
1173 Intersection,
1174 Coincident,
1175 Endpoint,
1176 }
1177
1178 #[derive(Debug, Clone)]
1179 struct Candidate {
1180 t: f64,
1181 point: Coords2d,
1182 candidate_type: CandidateType,
1183 segment_id: Option<ObjectId>,
1184 point_id: Option<ObjectId>,
1185 }
1186
1187 let mut candidates: Vec<Candidate> = Vec::new();
1188
1189 match segment {
1191 Segment::Line(line) => {
1192 candidates.push(Candidate {
1193 t: 0.0,
1194 point: segment_geometry.start,
1195 candidate_type: CandidateType::Endpoint,
1196 segment_id: None,
1197 point_id: Some(line.start),
1198 });
1199 candidates.push(Candidate {
1200 t: 1.0,
1201 point: segment_geometry.end,
1202 candidate_type: CandidateType::Endpoint,
1203 segment_id: None,
1204 point_id: Some(line.end),
1205 });
1206 }
1207 Segment::Arc(arc) => {
1208 candidates.push(Candidate {
1210 t: 0.0,
1211 point: segment_geometry.start,
1212 candidate_type: CandidateType::Endpoint,
1213 segment_id: None,
1214 point_id: Some(arc.start),
1215 });
1216 candidates.push(Candidate {
1217 t: 1.0,
1218 point: segment_geometry.end,
1219 candidate_type: CandidateType::Endpoint,
1220 segment_id: None,
1221 point_id: Some(arc.end),
1222 });
1223 }
1224 _ => {}
1225 }
1226
1227 let trim_spawn_seg_id = trim_spawn_seg.id;
1229
1230 for other_seg in objects.iter() {
1232 let other_id = other_seg.id;
1234
1235 if other_id == trim_spawn_seg_id {
1236 continue;
1237 }
1238
1239 let ObjectKind::Segment { segment: other_segment } = &other_seg.kind else {
1240 continue;
1241 };
1242
1243 match other_segment {
1245 Segment::Line(_) => {
1246 let other_start = get_position_coords_for_line(other_seg, LineEndpoint::Start, objects, default_unit);
1247 let other_end = get_position_coords_for_line(other_seg, LineEndpoint::End, objects, default_unit);
1248 if let (Some(os), Some(oe)) = (other_start, other_end) {
1249 match segment {
1250 Segment::Line(_) => {
1251 if let Some(intersection) = line_segment_intersection(
1252 segment_geometry.start,
1253 segment_geometry.end,
1254 os,
1255 oe,
1256 EPSILON_POINT_ON_SEGMENT,
1257 ) {
1258 let t = project_point_onto_segment(
1259 intersection,
1260 segment_geometry.start,
1261 segment_geometry.end,
1262 );
1263 candidates.push(Candidate {
1264 t,
1265 point: intersection,
1266 candidate_type: CandidateType::Intersection,
1267 segment_id: Some(other_id),
1268 point_id: None,
1269 });
1270 }
1271 }
1272 Segment::Arc(_) => {
1273 if let Some(center) = segment_geometry.center
1274 && let Some(intersection) = line_arc_intersection(
1275 os,
1276 oe,
1277 center,
1278 segment_geometry.start,
1279 segment_geometry.end,
1280 EPSILON_POINT_ON_SEGMENT,
1281 )
1282 {
1283 let t = project_point_onto_arc(
1284 intersection,
1285 center,
1286 segment_geometry.start,
1287 segment_geometry.end,
1288 );
1289 candidates.push(Candidate {
1290 t,
1291 point: intersection,
1292 candidate_type: CandidateType::Intersection,
1293 segment_id: Some(other_id),
1294 point_id: None,
1295 });
1296 }
1297 }
1298 _ => {}
1299 }
1300 }
1301 }
1302 Segment::Arc(_) => {
1303 let other_start = get_position_coords_from_arc(other_seg, ArcPoint::Start, objects, default_unit);
1304 let other_end = get_position_coords_from_arc(other_seg, ArcPoint::End, objects, default_unit);
1305 let other_center = get_position_coords_from_arc(other_seg, ArcPoint::Center, objects, default_unit);
1306 if let (Some(os), Some(oe), Some(oc)) = (other_start, other_end, other_center) {
1307 match segment {
1308 Segment::Line(_) => {
1309 if let Some(intersection) = line_arc_intersection(
1310 segment_geometry.start,
1311 segment_geometry.end,
1312 oc,
1313 os,
1314 oe,
1315 EPSILON_POINT_ON_SEGMENT,
1316 ) {
1317 let t = project_point_onto_segment(
1318 intersection,
1319 segment_geometry.start,
1320 segment_geometry.end,
1321 );
1322 candidates.push(Candidate {
1323 t,
1324 point: intersection,
1325 candidate_type: CandidateType::Intersection,
1326 segment_id: Some(other_id),
1327 point_id: None,
1328 });
1329 }
1330 }
1331 Segment::Arc(_) => {
1332 if let Some(center) = segment_geometry.center
1333 && let Some(intersection) = arc_arc_intersection(
1334 center,
1335 segment_geometry.start,
1336 segment_geometry.end,
1337 oc,
1338 os,
1339 oe,
1340 EPSILON_POINT_ON_SEGMENT,
1341 )
1342 {
1343 let t = project_point_onto_arc(
1344 intersection,
1345 center,
1346 segment_geometry.start,
1347 segment_geometry.end,
1348 );
1349 candidates.push(Candidate {
1350 t,
1351 point: intersection,
1352 candidate_type: CandidateType::Intersection,
1353 segment_id: Some(other_id),
1354 point_id: None,
1355 });
1356 }
1357 }
1358 _ => {}
1359 }
1360 }
1361 }
1362 _ => {}
1363 }
1364
1365 match other_segment {
1368 Segment::Line(line) => {
1369 let other_start_id = line.start;
1370 let other_end_id = line.end;
1371
1372 if is_point_coincident_with_segment_native(other_start_id, trim_spawn_seg_id, objects)
1374 && let Some(other_start) =
1375 get_position_coords_for_line(other_seg, LineEndpoint::Start, objects, default_unit)
1376 {
1377 let (t, is_on_segment) = match segment {
1378 Segment::Line(_) => {
1379 let t =
1380 project_point_onto_segment(other_start, segment_geometry.start, segment_geometry.end);
1381 let is_on = (0.0..=1.0).contains(&t)
1382 && perpendicular_distance_to_segment(
1383 other_start,
1384 segment_geometry.start,
1385 segment_geometry.end,
1386 ) <= EPSILON_POINT_ON_SEGMENT;
1387 (t, is_on)
1388 }
1389 Segment::Arc(_) => {
1390 if let Some(center) = segment_geometry.center {
1391 let t = project_point_onto_arc(
1392 other_start,
1393 center,
1394 segment_geometry.start,
1395 segment_geometry.end,
1396 );
1397 let is_on = is_point_on_arc(
1398 other_start,
1399 center,
1400 segment_geometry.start,
1401 segment_geometry.end,
1402 EPSILON_POINT_ON_SEGMENT,
1403 );
1404 (t, is_on)
1405 } else {
1406 continue;
1407 }
1408 }
1409 _ => continue,
1410 };
1411
1412 if is_on_segment {
1413 candidates.push(Candidate {
1414 t,
1415 point: other_start,
1416 candidate_type: CandidateType::Coincident,
1417 segment_id: Some(other_id),
1418 point_id: Some(other_start_id),
1419 });
1420 }
1421 }
1422
1423 if is_point_coincident_with_segment_native(other_end_id, trim_spawn_seg_id, objects)
1425 && let Some(other_end) =
1426 get_position_coords_for_line(other_seg, LineEndpoint::End, objects, default_unit)
1427 {
1428 let (t, is_on_segment) = match segment {
1429 Segment::Line(_) => {
1430 let t = project_point_onto_segment(other_end, segment_geometry.start, segment_geometry.end);
1431 let is_on = (0.0..=1.0).contains(&t)
1432 && perpendicular_distance_to_segment(
1433 other_end,
1434 segment_geometry.start,
1435 segment_geometry.end,
1436 ) <= EPSILON_POINT_ON_SEGMENT;
1437 (t, is_on)
1438 }
1439 Segment::Arc(_) => {
1440 if let Some(center) = segment_geometry.center {
1441 let t = project_point_onto_arc(
1442 other_end,
1443 center,
1444 segment_geometry.start,
1445 segment_geometry.end,
1446 );
1447 let is_on = is_point_on_arc(
1448 other_end,
1449 center,
1450 segment_geometry.start,
1451 segment_geometry.end,
1452 EPSILON_POINT_ON_SEGMENT,
1453 );
1454 (t, is_on)
1455 } else {
1456 continue;
1457 }
1458 }
1459 _ => continue,
1460 };
1461
1462 if is_on_segment {
1463 candidates.push(Candidate {
1464 t,
1465 point: other_end,
1466 candidate_type: CandidateType::Coincident,
1467 segment_id: Some(other_id),
1468 point_id: Some(other_end_id),
1469 });
1470 }
1471 }
1472 }
1473 Segment::Arc(arc) => {
1474 let other_start_id = arc.start;
1475 let other_end_id = arc.end;
1476
1477 if is_point_coincident_with_segment_native(other_start_id, trim_spawn_seg_id, objects)
1479 && let Some(other_start) =
1480 get_position_coords_from_arc(other_seg, ArcPoint::Start, objects, default_unit)
1481 {
1482 let (t, is_on_segment) = match segment {
1483 Segment::Line(_) => {
1484 let t =
1485 project_point_onto_segment(other_start, segment_geometry.start, segment_geometry.end);
1486 let is_on = (0.0..=1.0).contains(&t)
1487 && perpendicular_distance_to_segment(
1488 other_start,
1489 segment_geometry.start,
1490 segment_geometry.end,
1491 ) <= EPSILON_POINT_ON_SEGMENT;
1492 (t, is_on)
1493 }
1494 Segment::Arc(_) => {
1495 if let Some(center) = segment_geometry.center {
1496 let t = project_point_onto_arc(
1497 other_start,
1498 center,
1499 segment_geometry.start,
1500 segment_geometry.end,
1501 );
1502 let is_on = is_point_on_arc(
1503 other_start,
1504 center,
1505 segment_geometry.start,
1506 segment_geometry.end,
1507 EPSILON_POINT_ON_SEGMENT,
1508 );
1509 (t, is_on)
1510 } else {
1511 continue;
1512 }
1513 }
1514 _ => continue,
1515 };
1516
1517 if is_on_segment {
1518 candidates.push(Candidate {
1519 t,
1520 point: other_start,
1521 candidate_type: CandidateType::Coincident,
1522 segment_id: Some(other_id),
1523 point_id: Some(other_start_id),
1524 });
1525 }
1526 }
1527
1528 if is_point_coincident_with_segment_native(other_end_id, trim_spawn_seg_id, objects)
1530 && let Some(other_end) =
1531 get_position_coords_from_arc(other_seg, ArcPoint::End, objects, default_unit)
1532 {
1533 let (t, is_on_segment) = match segment {
1534 Segment::Line(_) => {
1535 let t = project_point_onto_segment(other_end, segment_geometry.start, segment_geometry.end);
1536 let is_on = (0.0..=1.0).contains(&t)
1537 && perpendicular_distance_to_segment(
1538 other_end,
1539 segment_geometry.start,
1540 segment_geometry.end,
1541 ) <= EPSILON_POINT_ON_SEGMENT;
1542 (t, is_on)
1543 }
1544 Segment::Arc(_) => {
1545 if let Some(center) = segment_geometry.center {
1546 let t = project_point_onto_arc(
1547 other_end,
1548 center,
1549 segment_geometry.start,
1550 segment_geometry.end,
1551 );
1552 let is_on = is_point_on_arc(
1553 other_end,
1554 center,
1555 segment_geometry.start,
1556 segment_geometry.end,
1557 EPSILON_POINT_ON_SEGMENT,
1558 );
1559 (t, is_on)
1560 } else {
1561 continue;
1562 }
1563 }
1564 _ => continue,
1565 };
1566
1567 if is_on_segment {
1568 candidates.push(Candidate {
1569 t,
1570 point: other_end,
1571 candidate_type: CandidateType::Coincident,
1572 segment_id: Some(other_id),
1573 point_id: Some(other_end_id),
1574 });
1575 }
1576 }
1577 }
1578 _ => {}
1579 }
1580 }
1581
1582 let intersection_epsilon = EPSILON_POINT_ON_SEGMENT * 10.0; let filtered_candidates: Vec<Candidate> = candidates
1586 .into_iter()
1587 .filter(|candidate| {
1588 let dist_from_intersection = (candidate.t - intersection_t).abs();
1589 if dist_from_intersection < intersection_epsilon {
1590 return false; }
1592
1593 match direction {
1594 TrimDirection::Left => candidate.t < intersection_t,
1595 TrimDirection::Right => candidate.t > intersection_t,
1596 }
1597 })
1598 .collect();
1599
1600 let mut sorted_candidates = filtered_candidates;
1603 sorted_candidates.sort_by(|a, b| {
1604 let dist_a = (a.t - intersection_t).abs();
1605 let dist_b = (b.t - intersection_t).abs();
1606 let dist_diff = dist_a - dist_b;
1607 if dist_diff.abs() > EPSILON_POINT_ON_SEGMENT {
1608 dist_diff.partial_cmp(&0.0).unwrap_or(std::cmp::Ordering::Equal)
1609 } else {
1610 let type_priority = |candidate_type: CandidateType| -> i32 {
1612 match candidate_type {
1613 CandidateType::Coincident => 0,
1614 CandidateType::Intersection => 1,
1615 CandidateType::Endpoint => 2,
1616 }
1617 };
1618 type_priority(a.candidate_type).cmp(&type_priority(b.candidate_type))
1619 }
1620 });
1621
1622 let closest_candidate = match sorted_candidates.first() {
1624 Some(c) => c,
1625 None => {
1626 let endpoint = match direction {
1628 TrimDirection::Left => segment_geometry.start,
1629 TrimDirection::Right => segment_geometry.end,
1630 };
1631 return Ok(TrimTermination::SegEndPoint {
1632 trim_termination_coords: endpoint,
1633 });
1634 }
1635 };
1636
1637 if closest_candidate.candidate_type == CandidateType::Intersection
1641 && let Some(seg_id) = closest_candidate.segment_id
1642 {
1643 let intersecting_seg = objects.iter().find(|obj| obj.id == seg_id);
1644
1645 if let Some(intersecting_seg) = intersecting_seg {
1646 let mut is_other_seg_endpoint = false;
1647 let endpoint_epsilon = EPSILON_POINT_ON_SEGMENT * 1000.0; if let ObjectKind::Segment { segment: other_segment } = &intersecting_seg.kind {
1651 match other_segment {
1652 Segment::Line(_) => {
1653 if let (Some(other_start), Some(other_end)) = (
1654 get_position_coords_for_line(intersecting_seg, LineEndpoint::Start, objects, default_unit),
1655 get_position_coords_for_line(intersecting_seg, LineEndpoint::End, objects, default_unit),
1656 ) {
1657 let dist_to_start = ((closest_candidate.point.x - other_start.x)
1658 * (closest_candidate.point.x - other_start.x)
1659 + (closest_candidate.point.y - other_start.y)
1660 * (closest_candidate.point.y - other_start.y))
1661 .sqrt();
1662 let dist_to_end = ((closest_candidate.point.x - other_end.x)
1663 * (closest_candidate.point.x - other_end.x)
1664 + (closest_candidate.point.y - other_end.y)
1665 * (closest_candidate.point.y - other_end.y))
1666 .sqrt();
1667 is_other_seg_endpoint = dist_to_start < endpoint_epsilon || dist_to_end < endpoint_epsilon;
1668 }
1669 }
1670 Segment::Arc(_) => {
1671 if let (Some(other_start), Some(other_end)) = (
1672 get_position_coords_from_arc(intersecting_seg, ArcPoint::Start, objects, default_unit),
1673 get_position_coords_from_arc(intersecting_seg, ArcPoint::End, objects, default_unit),
1674 ) {
1675 let dist_to_start = ((closest_candidate.point.x - other_start.x)
1676 * (closest_candidate.point.x - other_start.x)
1677 + (closest_candidate.point.y - other_start.y)
1678 * (closest_candidate.point.y - other_start.y))
1679 .sqrt();
1680 let dist_to_end = ((closest_candidate.point.x - other_end.x)
1681 * (closest_candidate.point.x - other_end.x)
1682 + (closest_candidate.point.y - other_end.y)
1683 * (closest_candidate.point.y - other_end.y))
1684 .sqrt();
1685 is_other_seg_endpoint = dist_to_start < endpoint_epsilon || dist_to_end < endpoint_epsilon;
1686 }
1687 }
1688 _ => {}
1689 }
1690 }
1691
1692 if is_other_seg_endpoint {
1695 let endpoint = match direction {
1696 TrimDirection::Left => segment_geometry.start,
1697 TrimDirection::Right => segment_geometry.end,
1698 };
1699 return Ok(TrimTermination::SegEndPoint {
1700 trim_termination_coords: endpoint,
1701 });
1702 }
1703 }
1704
1705 let endpoint_t = match direction {
1707 TrimDirection::Left => 0.0,
1708 TrimDirection::Right => 1.0,
1709 };
1710 let endpoint = match direction {
1711 TrimDirection::Left => segment_geometry.start,
1712 TrimDirection::Right => segment_geometry.end,
1713 };
1714 let dist_to_endpoint_param = (closest_candidate.t - endpoint_t).abs();
1715 let dist_to_endpoint_coords = ((closest_candidate.point.x - endpoint.x)
1716 * (closest_candidate.point.x - endpoint.x)
1717 + (closest_candidate.point.y - endpoint.y) * (closest_candidate.point.y - endpoint.y))
1718 .sqrt();
1719
1720 let is_at_endpoint =
1721 dist_to_endpoint_param < EPSILON_POINT_ON_SEGMENT || dist_to_endpoint_coords < EPSILON_POINT_ON_SEGMENT;
1722
1723 if is_at_endpoint {
1724 return Ok(TrimTermination::SegEndPoint {
1726 trim_termination_coords: endpoint,
1727 });
1728 }
1729 }
1730
1731 let endpoint_t_for_return = match direction {
1733 TrimDirection::Left => 0.0,
1734 TrimDirection::Right => 1.0,
1735 };
1736 if closest_candidate.candidate_type == CandidateType::Intersection {
1737 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
1738 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
1739 let endpoint = match direction {
1742 TrimDirection::Left => segment_geometry.start,
1743 TrimDirection::Right => segment_geometry.end,
1744 };
1745 return Ok(TrimTermination::SegEndPoint {
1746 trim_termination_coords: endpoint,
1747 });
1748 }
1749 }
1750
1751 let endpoint = match direction {
1753 TrimDirection::Left => segment_geometry.start,
1754 TrimDirection::Right => segment_geometry.end,
1755 };
1756 if closest_candidate.candidate_type == CandidateType::Endpoint {
1757 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
1758 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
1759 return Ok(TrimTermination::SegEndPoint {
1761 trim_termination_coords: endpoint,
1762 });
1763 }
1764 }
1765
1766 if closest_candidate.candidate_type == CandidateType::Coincident {
1768 Ok(TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
1770 trim_termination_coords: closest_candidate.point,
1771 intersecting_seg_id: closest_candidate
1772 .segment_id
1773 .ok_or_else(|| "Missing segment_id for coincident".to_string())?,
1774 other_segment_point_id: closest_candidate
1775 .point_id
1776 .ok_or_else(|| "Missing point_id for coincident".to_string())?,
1777 })
1778 } else if closest_candidate.candidate_type == CandidateType::Intersection {
1779 Ok(TrimTermination::Intersection {
1780 trim_termination_coords: closest_candidate.point,
1781 intersecting_seg_id: closest_candidate
1782 .segment_id
1783 .ok_or_else(|| "Missing segment_id for intersection".to_string())?,
1784 })
1785 } else {
1786 Ok(TrimTermination::SegEndPoint {
1788 trim_termination_coords: closest_candidate.point,
1789 })
1790 }
1791}
1792
1793#[cfg(test)]
1804#[allow(dead_code)]
1805pub(crate) async fn execute_trim_loop<F, Fut>(
1806 points: &[Coords2d],
1807 default_unit: UnitLength,
1808 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
1809 mut execute_operations: F,
1810) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>
1811where
1812 F: FnMut(Vec<TrimOperation>, crate::frontend::api::SceneGraphDelta) -> Fut,
1813 Fut: std::future::Future<
1814 Output = Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>,
1815 >,
1816{
1817 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
1819 let points = normalized_points.as_slice();
1820
1821 let mut start_index = 0;
1822 let max_iterations = 1000;
1823 let mut iteration_count = 0;
1824 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
1825 crate::frontend::api::SourceDelta { text: String::new() },
1826 initial_scene_graph_delta.clone(),
1827 ));
1828 let mut invalidates_ids = false;
1829 let mut current_scene_graph_delta = initial_scene_graph_delta;
1830
1831 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
1832 iteration_count += 1;
1833
1834 let next_trim_spawn = get_next_trim_spawn(
1836 points,
1837 start_index,
1838 ¤t_scene_graph_delta.new_graph.objects,
1839 default_unit,
1840 );
1841
1842 match &next_trim_spawn {
1843 TrimItem::None { next_index } => {
1844 let old_start_index = start_index;
1845 start_index = *next_index;
1846
1847 if start_index <= old_start_index {
1849 start_index = old_start_index + 1;
1850 }
1851
1852 if start_index >= points.len().saturating_sub(1) {
1854 break;
1855 }
1856 continue;
1857 }
1858 TrimItem::Spawn {
1859 trim_spawn_seg_id,
1860 next_index,
1861 ..
1862 } => {
1863 let terminations = match get_trim_spawn_terminations(
1865 *trim_spawn_seg_id,
1866 points,
1867 ¤t_scene_graph_delta.new_graph.objects,
1868 default_unit,
1869 ) {
1870 Ok(terms) => terms,
1871 Err(e) => {
1872 crate::logln!("Error getting trim spawn terminations: {}", e);
1873 let old_start_index = start_index;
1874 start_index = *next_index;
1875 if start_index <= old_start_index {
1876 start_index = old_start_index + 1;
1877 }
1878 continue;
1879 }
1880 };
1881
1882 let trim_spawn_segment = current_scene_graph_delta
1884 .new_graph
1885 .objects
1886 .iter()
1887 .find(|obj| obj.id == *trim_spawn_seg_id)
1888 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
1889
1890 let strategy = match trim_strategy(
1891 *trim_spawn_seg_id,
1892 trim_spawn_segment,
1893 &terminations.left_side,
1894 &terminations.right_side,
1895 ¤t_scene_graph_delta.new_graph.objects,
1896 default_unit,
1897 ) {
1898 Ok(ops) => ops,
1899 Err(e) => {
1900 crate::logln!("Error determining trim strategy: {}", e);
1901 let old_start_index = start_index;
1902 start_index = *next_index;
1903 if start_index <= old_start_index {
1904 start_index = old_start_index + 1;
1905 }
1906 continue;
1907 }
1908 };
1909
1910 let was_deleted = strategy.iter().any(|op| matches!(op, TrimOperation::SimpleTrim { .. }));
1912
1913 match execute_operations(strategy, current_scene_graph_delta.clone()).await {
1915 Ok((source_delta, scene_graph_delta)) => {
1916 last_result = Some((source_delta, scene_graph_delta.clone()));
1917 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
1918 current_scene_graph_delta = scene_graph_delta;
1919 }
1920 Err(e) => {
1921 crate::logln!("Error executing trim operations: {}", e);
1922 }
1924 }
1925
1926 let old_start_index = start_index;
1928 start_index = *next_index;
1929
1930 if start_index <= old_start_index && !was_deleted {
1932 start_index = old_start_index + 1;
1933 }
1934 }
1935 }
1936 }
1937
1938 if iteration_count >= max_iterations {
1939 return Err(format!("Reached max iterations ({})", max_iterations));
1940 }
1941
1942 last_result.ok_or_else(|| "No trim operations were executed".to_string())
1944}
1945
1946#[cfg(test)]
1948#[derive(Debug, Clone)]
1949pub struct TrimFlowResult {
1950 pub kcl_code: String,
1951 pub invalidates_ids: bool,
1952}
1953
1954#[cfg(all(not(target_arch = "wasm32"), test))]
1970pub(crate) async fn execute_trim_flow(
1971 kcl_code: &str,
1972 trim_points: &[Coords2d],
1973 sketch_id: ObjectId,
1974) -> Result<TrimFlowResult, String> {
1975 use crate::{
1976 ExecutorContext, Program,
1977 frontend::{FrontendState, api::Version},
1978 };
1979
1980 let parse_result = Program::parse(kcl_code).map_err(|e| format!("Failed to parse KCL: {}", e))?;
1982 let (program_opt, errors) = parse_result;
1983 if !errors.is_empty() {
1984 return Err(format!("Failed to parse KCL: {:?}", errors));
1985 }
1986 let program = program_opt.ok_or_else(|| "No AST produced".to_string())?;
1987
1988 let ctx = ExecutorContext::new_with_default_client()
1989 .await
1990 .map_err(|e| format!("Failed to create executor context: {}", e))?;
1991
1992 let mock_ctx = ExecutorContext::new_mock(None).await;
1993
1994 let result = async {
1996 let mut frontend = FrontendState::new();
1997
1998 frontend.program = program.clone();
2000
2001 let exec_outcome = ctx
2002 .run_with_caching(program.clone())
2003 .await
2004 .map_err(|e| format!("Failed to execute program: {}", e.error.message()))?;
2005
2006 let exec_outcome = frontend.update_state_after_exec(exec_outcome, false);
2007 #[allow(unused_mut)] let mut initial_scene_graph = frontend.scene_graph.clone();
2009
2010 #[cfg(feature = "artifact-graph")]
2013 if initial_scene_graph.objects.is_empty() && !exec_outcome.scene_objects.is_empty() {
2014 initial_scene_graph.objects = exec_outcome.scene_objects.clone();
2015 }
2016
2017 let actual_sketch_id = if let Some(sketch_mode) = initial_scene_graph.sketch_mode {
2020 sketch_mode
2021 } else {
2022 initial_scene_graph
2024 .objects
2025 .iter()
2026 .find(|obj| matches!(obj.kind, crate::frontend::api::ObjectKind::Sketch { .. }))
2027 .map(|obj| obj.id)
2028 .unwrap_or(sketch_id) };
2030
2031 let version = Version(0);
2032 let initial_scene_graph_delta = crate::frontend::api::SceneGraphDelta {
2033 new_graph: initial_scene_graph,
2034 new_objects: vec![],
2035 invalidates_ids: false,
2036 exec_outcome,
2037 };
2038
2039 let (source_delta, scene_graph_delta) = execute_trim_loop_with_context(
2044 trim_points,
2045 initial_scene_graph_delta,
2046 &mut frontend,
2047 &mock_ctx,
2048 version,
2049 actual_sketch_id,
2050 )
2051 .await?;
2052
2053 if source_delta.text.is_empty() {
2056 return Err("No trim operations were executed - source delta is empty".to_string());
2057 }
2058
2059 Ok(TrimFlowResult {
2060 kcl_code: source_delta.text,
2061 invalidates_ids: scene_graph_delta.invalidates_ids,
2062 })
2063 }
2064 .await;
2065
2066 ctx.close().await;
2068 mock_ctx.close().await;
2069
2070 result
2071}
2072
2073pub async fn execute_trim_loop_with_context(
2079 points: &[Coords2d],
2080 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2081 frontend: &mut crate::frontend::FrontendState,
2082 ctx: &crate::ExecutorContext,
2083 version: crate::frontend::api::Version,
2084 sketch_id: ObjectId,
2085) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
2086 let default_unit = frontend.default_length_unit();
2088 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2089
2090 let mut current_scene_graph_delta = initial_scene_graph_delta.clone();
2093 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2094 crate::frontend::api::SourceDelta { text: String::new() },
2095 initial_scene_graph_delta.clone(),
2096 ));
2097 let mut invalidates_ids = false;
2098 let mut start_index = 0;
2099 let max_iterations = 1000;
2100 let mut iteration_count = 0;
2101
2102 let points = normalized_points.as_slice();
2103
2104 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2105 iteration_count += 1;
2106
2107 let next_trim_spawn = get_next_trim_spawn(
2109 points,
2110 start_index,
2111 ¤t_scene_graph_delta.new_graph.objects,
2112 default_unit,
2113 );
2114
2115 match &next_trim_spawn {
2116 TrimItem::None { next_index } => {
2117 let old_start_index = start_index;
2118 start_index = *next_index;
2119 if start_index <= old_start_index {
2120 start_index = old_start_index + 1;
2121 }
2122 if start_index >= points.len().saturating_sub(1) {
2123 break;
2124 }
2125 continue;
2126 }
2127 TrimItem::Spawn {
2128 trim_spawn_seg_id,
2129 next_index,
2130 ..
2131 } => {
2132 let terminations = match get_trim_spawn_terminations(
2134 *trim_spawn_seg_id,
2135 points,
2136 ¤t_scene_graph_delta.new_graph.objects,
2137 default_unit,
2138 ) {
2139 Ok(terms) => terms,
2140 Err(e) => {
2141 crate::logln!("Error getting trim spawn terminations: {}", e);
2142 let old_start_index = start_index;
2143 start_index = *next_index;
2144 if start_index <= old_start_index {
2145 start_index = old_start_index + 1;
2146 }
2147 continue;
2148 }
2149 };
2150
2151 let trim_spawn_segment = current_scene_graph_delta
2153 .new_graph
2154 .objects
2155 .iter()
2156 .find(|obj| obj.id == *trim_spawn_seg_id)
2157 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
2158
2159 let strategy = match trim_strategy(
2160 *trim_spawn_seg_id,
2161 trim_spawn_segment,
2162 &terminations.left_side,
2163 &terminations.right_side,
2164 ¤t_scene_graph_delta.new_graph.objects,
2165 default_unit,
2166 ) {
2167 Ok(ops) => ops,
2168 Err(e) => {
2169 crate::logln!("Error determining trim strategy: {}", e);
2170 let old_start_index = start_index;
2171 start_index = *next_index;
2172 if start_index <= old_start_index {
2173 start_index = old_start_index + 1;
2174 }
2175 continue;
2176 }
2177 };
2178
2179 let was_deleted = strategy.iter().any(|op| matches!(op, TrimOperation::SimpleTrim { .. }));
2181
2182 match execute_trim_operations_simple(
2184 strategy.clone(),
2185 ¤t_scene_graph_delta,
2186 frontend,
2187 ctx,
2188 version,
2189 sketch_id,
2190 )
2191 .await
2192 {
2193 Ok((source_delta, scene_graph_delta)) => {
2194 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2195 last_result = Some((source_delta, scene_graph_delta.clone()));
2196 current_scene_graph_delta = scene_graph_delta;
2197 }
2198 Err(e) => {
2199 crate::logln!("Error executing trim operations: {}", e);
2200 }
2201 }
2202
2203 let old_start_index = start_index;
2205 start_index = *next_index;
2206 if start_index <= old_start_index && !was_deleted {
2207 start_index = old_start_index + 1;
2208 }
2209 }
2210 }
2211 }
2212
2213 if iteration_count >= max_iterations {
2214 return Err(format!("Reached max iterations ({})", max_iterations));
2215 }
2216
2217 let (source_delta, mut scene_graph_delta) =
2218 last_result.ok_or_else(|| "No trim operations were executed".to_string())?;
2219 scene_graph_delta.invalidates_ids = invalidates_ids;
2221 Ok((source_delta, scene_graph_delta))
2222}
2223
2224pub(crate) fn trim_strategy(
2284 trim_spawn_id: ObjectId,
2285 trim_spawn_segment: &Object,
2286 left_side: &TrimTermination,
2287 right_side: &TrimTermination,
2288 objects: &[Object],
2289 default_unit: UnitLength,
2290) -> Result<Vec<TrimOperation>, String> {
2291 if matches!(left_side, TrimTermination::SegEndPoint { .. })
2293 && matches!(right_side, TrimTermination::SegEndPoint { .. })
2294 {
2295 return Ok(vec![TrimOperation::SimpleTrim {
2296 segment_to_trim_id: trim_spawn_id,
2297 }]);
2298 }
2299
2300 let is_intersect_or_coincident = |side: &TrimTermination| -> bool {
2302 matches!(
2303 side,
2304 TrimTermination::Intersection { .. }
2305 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
2306 )
2307 };
2308
2309 let left_side_needs_tail_cut = is_intersect_or_coincident(left_side) && !is_intersect_or_coincident(right_side);
2310 let right_side_needs_tail_cut = is_intersect_or_coincident(right_side) && !is_intersect_or_coincident(left_side);
2311
2312 let ObjectKind::Segment { segment } = &trim_spawn_segment.kind else {
2314 return Err("Trim spawn segment is not a segment".to_string());
2315 };
2316
2317 let (_segment_type, ctor) = match segment {
2318 Segment::Line(line) => ("Line", &line.ctor),
2319 Segment::Arc(arc) => ("Arc", &arc.ctor),
2320 _ => {
2321 return Err("Trim spawn segment is not a Line or Arc".to_string());
2322 }
2323 };
2324
2325 let units = match ctor {
2327 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
2328 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2329 _ => NumericSuffix::Mm,
2330 },
2331 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
2332 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2333 _ => NumericSuffix::Mm,
2334 },
2335 _ => NumericSuffix::Mm,
2336 };
2337
2338 let find_distance_constraints_for_segment = |segment_id: ObjectId| -> Vec<ObjectId> {
2340 let mut constraint_ids = Vec::new();
2341 for obj in objects {
2342 let ObjectKind::Constraint { constraint } = &obj.kind else {
2343 continue;
2344 };
2345
2346 let Constraint::Distance(distance) = constraint else {
2347 continue;
2348 };
2349
2350 let points_owned_by_segment: Vec<bool> = distance
2356 .points
2357 .iter()
2358 .map(|point_id| {
2359 if let Some(point_obj) = objects.iter().find(|o| o.id == *point_id)
2360 && let ObjectKind::Segment { segment } = &point_obj.kind
2361 && let Segment::Point(point) = segment
2362 && let Some(owner_id) = point.owner
2363 {
2364 return owner_id == segment_id;
2365 }
2366 false
2367 })
2368 .collect();
2369
2370 if points_owned_by_segment.len() == 2 && points_owned_by_segment.iter().all(|&owned| owned) {
2372 constraint_ids.push(obj.id);
2373 }
2374 }
2375 constraint_ids
2376 };
2377
2378 let find_existing_point_segment_coincident =
2380 |trim_seg_id: ObjectId, intersecting_seg_id: ObjectId| -> CoincidentData {
2381 let lookup_by_point_id = |point_id: ObjectId| -> Option<CoincidentData> {
2383 for obj in objects {
2384 let ObjectKind::Constraint { constraint } = &obj.kind else {
2385 continue;
2386 };
2387
2388 let Constraint::Coincident(coincident) = constraint else {
2389 continue;
2390 };
2391
2392 let involves_trim_seg = coincident
2393 .segments
2394 .iter()
2395 .any(|id| *id == trim_seg_id || *id == point_id);
2396 let involves_point = coincident.segments.contains(&point_id);
2397
2398 if involves_trim_seg && involves_point {
2399 return Some(CoincidentData {
2400 intersecting_seg_id,
2401 intersecting_endpoint_point_id: Some(point_id),
2402 existing_point_segment_constraint_id: Some(obj.id),
2403 });
2404 }
2405 }
2406 None
2407 };
2408
2409 let trim_seg = objects.iter().find(|obj| obj.id == trim_seg_id);
2411
2412 let mut trim_endpoint_ids: Vec<ObjectId> = Vec::new();
2413 if let Some(seg) = trim_seg
2414 && let ObjectKind::Segment { segment } = &seg.kind
2415 {
2416 match segment {
2417 Segment::Line(line) => {
2418 trim_endpoint_ids.push(line.start);
2419 trim_endpoint_ids.push(line.end);
2420 }
2421 Segment::Arc(arc) => {
2422 trim_endpoint_ids.push(arc.start);
2423 trim_endpoint_ids.push(arc.end);
2424 }
2425 _ => {}
2426 }
2427 }
2428
2429 let intersecting_obj = objects.iter().find(|obj| obj.id == intersecting_seg_id);
2430
2431 if let Some(obj) = intersecting_obj
2432 && let ObjectKind::Segment { segment } = &obj.kind
2433 && let Segment::Point(_) = segment
2434 && let Some(found) = lookup_by_point_id(intersecting_seg_id)
2435 {
2436 return found;
2437 }
2438
2439 let mut intersecting_endpoint_ids: Vec<ObjectId> = Vec::new();
2441 if let Some(obj) = intersecting_obj
2442 && let ObjectKind::Segment { segment } = &obj.kind
2443 {
2444 match segment {
2445 Segment::Line(line) => {
2446 intersecting_endpoint_ids.push(line.start);
2447 intersecting_endpoint_ids.push(line.end);
2448 }
2449 Segment::Arc(arc) => {
2450 intersecting_endpoint_ids.push(arc.start);
2451 intersecting_endpoint_ids.push(arc.end);
2452 }
2453 _ => {}
2454 }
2455 }
2456
2457 intersecting_endpoint_ids.push(intersecting_seg_id);
2459
2460 for obj in objects {
2462 let ObjectKind::Constraint { constraint } = &obj.kind else {
2463 continue;
2464 };
2465
2466 let Constraint::Coincident(coincident) = constraint else {
2467 continue;
2468 };
2469
2470 let constraint_segment_ids: Vec<ObjectId> = coincident.segments.to_vec();
2471
2472 let involves_trim_seg = constraint_segment_ids.contains(&trim_seg_id)
2474 || trim_endpoint_ids.iter().any(|&id| constraint_segment_ids.contains(&id));
2475
2476 if !involves_trim_seg {
2477 continue;
2478 }
2479
2480 if let Some(&intersecting_endpoint_id) = intersecting_endpoint_ids
2482 .iter()
2483 .find(|&&id| constraint_segment_ids.contains(&id))
2484 {
2485 return CoincidentData {
2486 intersecting_seg_id,
2487 intersecting_endpoint_point_id: Some(intersecting_endpoint_id),
2488 existing_point_segment_constraint_id: Some(obj.id),
2489 };
2490 }
2491 }
2492
2493 CoincidentData {
2495 intersecting_seg_id,
2496 intersecting_endpoint_point_id: None,
2497 existing_point_segment_constraint_id: None,
2498 }
2499 };
2500
2501 let find_point_segment_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<serde_json::Value> {
2503 let mut constraints: Vec<serde_json::Value> = Vec::new();
2504 for obj in objects {
2505 let ObjectKind::Constraint { constraint } = &obj.kind else {
2506 continue;
2507 };
2508
2509 let Constraint::Coincident(coincident) = constraint else {
2510 continue;
2511 };
2512
2513 if !coincident.segments.contains(&endpoint_point_id) {
2515 continue;
2516 }
2517
2518 let other_segment_id = coincident.segments.iter().find_map(|seg_id| {
2520 if *seg_id != endpoint_point_id {
2521 Some(*seg_id)
2522 } else {
2523 None
2524 }
2525 });
2526
2527 if let Some(other_id) = other_segment_id
2528 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
2529 {
2530 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
2532 constraints.push(serde_json::json!({
2533 "constraintId": obj.id.0,
2534 "segmentOrPointId": other_id.0,
2535 }));
2536 }
2537 }
2538 }
2539 constraints
2540 };
2541
2542 let find_point_point_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
2545 let mut constraint_ids = Vec::new();
2546 for obj in objects {
2547 let ObjectKind::Constraint { constraint } = &obj.kind else {
2548 continue;
2549 };
2550
2551 let Constraint::Coincident(coincident) = constraint else {
2552 continue;
2553 };
2554
2555 if !coincident.segments.contains(&endpoint_point_id) {
2557 continue;
2558 }
2559
2560 let is_point_point = coincident.segments.iter().all(|seg_id| {
2562 if let Some(seg_obj) = objects.iter().find(|o| o.id == *seg_id) {
2563 matches!(&seg_obj.kind, ObjectKind::Segment { segment } if matches!(segment, Segment::Point(_)))
2564 } else {
2565 false
2566 }
2567 });
2568
2569 if is_point_point {
2570 constraint_ids.push(obj.id);
2571 }
2572 }
2573 constraint_ids
2574 };
2575
2576 let find_point_segment_coincident_constraint_ids = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
2579 let mut constraint_ids = Vec::new();
2580 for obj in objects {
2581 let ObjectKind::Constraint { constraint } = &obj.kind else {
2582 continue;
2583 };
2584
2585 let Constraint::Coincident(coincident) = constraint else {
2586 continue;
2587 };
2588
2589 if !coincident.segments.contains(&endpoint_point_id) {
2591 continue;
2592 }
2593
2594 let other_segment_id = coincident.segments.iter().find_map(|seg_id| {
2596 if *seg_id != endpoint_point_id {
2597 Some(*seg_id)
2598 } else {
2599 None
2600 }
2601 });
2602
2603 if let Some(other_id) = other_segment_id
2604 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
2605 {
2606 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
2608 constraint_ids.push(obj.id);
2609 }
2610 }
2611 }
2612 constraint_ids
2613 };
2614
2615 if left_side_needs_tail_cut || right_side_needs_tail_cut {
2617 let side = if left_side_needs_tail_cut {
2618 left_side
2619 } else {
2620 right_side
2621 };
2622
2623 let intersection_coords = match side {
2624 TrimTermination::Intersection {
2625 trim_termination_coords,
2626 ..
2627 }
2628 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2629 trim_termination_coords,
2630 ..
2631 } => *trim_termination_coords,
2632 TrimTermination::SegEndPoint { .. } => {
2633 return Err("Logic error: side should not be segEndPoint here".to_string());
2634 }
2635 };
2636
2637 let endpoint_to_change = if left_side_needs_tail_cut {
2638 EndpointChanged::End
2639 } else {
2640 EndpointChanged::Start
2641 };
2642
2643 let intersecting_seg_id = match side {
2644 TrimTermination::Intersection {
2645 intersecting_seg_id, ..
2646 }
2647 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2648 intersecting_seg_id, ..
2649 } => *intersecting_seg_id,
2650 TrimTermination::SegEndPoint { .. } => {
2651 return Err("Logic error".to_string());
2652 }
2653 };
2654
2655 let coincident_data = if matches!(
2656 side,
2657 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
2658 ) {
2659 let point_id = match side {
2660 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2661 other_segment_point_id, ..
2662 } => *other_segment_point_id,
2663 _ => return Err("Logic error".to_string()),
2664 };
2665 let mut data = find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id);
2666 data.intersecting_endpoint_point_id = Some(point_id);
2667 data
2668 } else {
2669 find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id)
2670 };
2671
2672 let trim_seg = objects.iter().find(|obj| obj.id == trim_spawn_id);
2674
2675 let endpoint_point_id = if let Some(seg) = trim_seg {
2676 let ObjectKind::Segment { segment } = &seg.kind else {
2677 return Err("Trim spawn segment is not a segment".to_string());
2678 };
2679 match segment {
2680 Segment::Line(line) => {
2681 if endpoint_to_change == EndpointChanged::Start {
2682 Some(line.start)
2683 } else {
2684 Some(line.end)
2685 }
2686 }
2687 Segment::Arc(arc) => {
2688 if endpoint_to_change == EndpointChanged::Start {
2689 Some(arc.start)
2690 } else {
2691 Some(arc.end)
2692 }
2693 }
2694 _ => None,
2695 }
2696 } else {
2697 None
2698 };
2699
2700 let coincident_end_constraint_to_delete_ids = if let Some(point_id) = endpoint_point_id {
2702 let mut constraint_ids = find_point_point_coincident_constraints(point_id);
2703 constraint_ids.extend(find_point_segment_coincident_constraint_ids(point_id));
2705 constraint_ids
2706 } else {
2707 Vec::new()
2708 };
2709
2710 let mut operations: Vec<TrimOperation> = Vec::new();
2711
2712 let new_ctor = match ctor {
2714 SegmentCtor::Line(line_ctor) => {
2715 let new_point = crate::frontend::sketch::Point2d {
2717 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
2718 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
2719 };
2720 if endpoint_to_change == EndpointChanged::Start {
2721 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
2722 start: new_point,
2723 end: line_ctor.end.clone(),
2724 construction: line_ctor.construction,
2725 })
2726 } else {
2727 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
2728 start: line_ctor.start.clone(),
2729 end: new_point,
2730 construction: line_ctor.construction,
2731 })
2732 }
2733 }
2734 SegmentCtor::Arc(arc_ctor) => {
2735 let new_point = crate::frontend::sketch::Point2d {
2737 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
2738 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
2739 };
2740 if endpoint_to_change == EndpointChanged::Start {
2741 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
2742 start: new_point,
2743 end: arc_ctor.end.clone(),
2744 center: arc_ctor.center.clone(),
2745 construction: arc_ctor.construction,
2746 })
2747 } else {
2748 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
2749 start: arc_ctor.start.clone(),
2750 end: new_point,
2751 center: arc_ctor.center.clone(),
2752 construction: arc_ctor.construction,
2753 })
2754 }
2755 }
2756 _ => {
2757 return Err("Unsupported segment type for edit".to_string());
2758 }
2759 };
2760 operations.push(TrimOperation::EditSegment {
2761 segment_id: trim_spawn_id,
2762 ctor: new_ctor,
2763 endpoint_changed: endpoint_to_change,
2764 });
2765
2766 let add_coincident = TrimOperation::AddCoincidentConstraint {
2768 segment_id: trim_spawn_id,
2769 endpoint_changed: endpoint_to_change,
2770 segment_or_point_to_make_coincident_to: intersecting_seg_id,
2771 intersecting_endpoint_point_id: coincident_data.intersecting_endpoint_point_id,
2772 };
2773 operations.push(add_coincident);
2774
2775 let mut all_constraint_ids_to_delete: Vec<ObjectId> = Vec::new();
2777 if let Some(constraint_id) = coincident_data.existing_point_segment_constraint_id {
2778 all_constraint_ids_to_delete.push(constraint_id);
2779 }
2780 all_constraint_ids_to_delete.extend(coincident_end_constraint_to_delete_ids);
2781
2782 let distance_constraint_ids = find_distance_constraints_for_segment(trim_spawn_id);
2785 all_constraint_ids_to_delete.extend(distance_constraint_ids);
2786
2787 if !all_constraint_ids_to_delete.is_empty() {
2788 operations.push(TrimOperation::DeleteConstraints {
2789 constraint_ids: all_constraint_ids_to_delete,
2790 });
2791 }
2792
2793 return Ok(operations);
2794 }
2795
2796 let left_side_intersects = is_intersect_or_coincident(left_side);
2798 let right_side_intersects = is_intersect_or_coincident(right_side);
2799
2800 if left_side_intersects && right_side_intersects {
2801 let left_intersecting_seg_id = match left_side {
2804 TrimTermination::Intersection {
2805 intersecting_seg_id, ..
2806 }
2807 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2808 intersecting_seg_id, ..
2809 } => *intersecting_seg_id,
2810 TrimTermination::SegEndPoint { .. } => {
2811 return Err("Logic error: left side should not be segEndPoint".to_string());
2812 }
2813 };
2814
2815 let right_intersecting_seg_id = match right_side {
2816 TrimTermination::Intersection {
2817 intersecting_seg_id, ..
2818 }
2819 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2820 intersecting_seg_id, ..
2821 } => *intersecting_seg_id,
2822 TrimTermination::SegEndPoint { .. } => {
2823 return Err("Logic error: right side should not be segEndPoint".to_string());
2824 }
2825 };
2826
2827 let left_coincident_data = if matches!(
2828 left_side,
2829 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
2830 ) {
2831 let point_id = match left_side {
2832 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2833 other_segment_point_id, ..
2834 } => *other_segment_point_id,
2835 _ => return Err("Logic error".to_string()),
2836 };
2837 let mut data = find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id);
2838 data.intersecting_endpoint_point_id = Some(point_id);
2839 data
2840 } else {
2841 find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id)
2842 };
2843
2844 let right_coincident_data = if matches!(
2845 right_side,
2846 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
2847 ) {
2848 let point_id = match right_side {
2849 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2850 other_segment_point_id, ..
2851 } => *other_segment_point_id,
2852 _ => return Err("Logic error".to_string()),
2853 };
2854 let mut data = find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id);
2855 data.intersecting_endpoint_point_id = Some(point_id);
2856 data
2857 } else {
2858 find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id)
2859 };
2860
2861 let (original_start_point_id, original_end_point_id) = match segment {
2863 Segment::Line(line) => (Some(line.start), Some(line.end)),
2864 Segment::Arc(arc) => (Some(arc.start), Some(arc.end)),
2865 _ => (None, None),
2866 };
2867
2868 let original_end_point_coords = match segment {
2870 Segment::Line(_) => {
2871 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
2872 }
2873 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
2874 _ => None,
2875 };
2876
2877 let Some(original_end_coords) = original_end_point_coords else {
2878 return Err(
2879 "Could not get original end point coordinates before editing - this is required for split trim"
2880 .to_string(),
2881 );
2882 };
2883
2884 let left_trim_coords = match left_side {
2886 TrimTermination::SegEndPoint {
2887 trim_termination_coords,
2888 }
2889 | TrimTermination::Intersection {
2890 trim_termination_coords,
2891 ..
2892 }
2893 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2894 trim_termination_coords,
2895 ..
2896 } => *trim_termination_coords,
2897 };
2898
2899 let right_trim_coords = match right_side {
2900 TrimTermination::SegEndPoint {
2901 trim_termination_coords,
2902 }
2903 | TrimTermination::Intersection {
2904 trim_termination_coords,
2905 ..
2906 }
2907 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2908 trim_termination_coords,
2909 ..
2910 } => *trim_termination_coords,
2911 };
2912
2913 let dist_to_original_end = ((right_trim_coords.x - original_end_coords.x)
2915 * (right_trim_coords.x - original_end_coords.x)
2916 + (right_trim_coords.y - original_end_coords.y) * (right_trim_coords.y - original_end_coords.y))
2917 .sqrt();
2918 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
2919 return Err(
2920 "Split point is at original end point - this should be handled as cutTail, not split".to_string(),
2921 );
2922 }
2923
2924 let mut constraints_to_migrate: Vec<ConstraintToMigrate> = Vec::new();
2927 let mut constraints_to_delete_set: IndexSet<ObjectId> = IndexSet::new();
2928
2929 if let Some(constraint_id) = left_coincident_data.existing_point_segment_constraint_id {
2931 constraints_to_delete_set.insert(constraint_id);
2932 }
2933 if let Some(constraint_id) = right_coincident_data.existing_point_segment_constraint_id {
2934 constraints_to_delete_set.insert(constraint_id);
2935 }
2936
2937 if let Some(end_id) = original_end_point_id {
2939 let end_point_point_constraint_ids = find_point_point_coincident_constraints(end_id);
2940 for constraint_id in end_point_point_constraint_ids {
2941 let other_point_id_opt = objects.iter().find_map(|obj| {
2943 if obj.id != constraint_id {
2944 return None;
2945 }
2946 let ObjectKind::Constraint { constraint } = &obj.kind else {
2947 return None;
2948 };
2949 let Constraint::Coincident(coincident) = constraint else {
2950 return None;
2951 };
2952 coincident
2953 .segments
2954 .iter()
2955 .find_map(|seg_id| if *seg_id != end_id { Some(*seg_id) } else { None })
2956 });
2957
2958 if let Some(other_point_id) = other_point_id_opt {
2959 constraints_to_delete_set.insert(constraint_id);
2960 constraints_to_migrate.push(ConstraintToMigrate {
2962 constraint_id,
2963 other_entity_id: other_point_id,
2964 is_point_point: true,
2965 attach_to_endpoint: AttachToEndpoint::End,
2966 });
2967 }
2968 }
2969 }
2970
2971 if let Some(end_id) = original_end_point_id {
2973 let end_point_segment_constraints = find_point_segment_coincident_constraints(end_id);
2974 for constraint_json in end_point_segment_constraints {
2975 if let Some(constraint_id_usize) = constraint_json
2976 .get("constraintId")
2977 .and_then(|v| v.as_u64())
2978 .map(|id| id as usize)
2979 {
2980 let constraint_id = ObjectId(constraint_id_usize);
2981 constraints_to_delete_set.insert(constraint_id);
2982 if let Some(other_id_usize) = constraint_json
2984 .get("segmentOrPointId")
2985 .and_then(|v| v.as_u64())
2986 .map(|id| id as usize)
2987 {
2988 constraints_to_migrate.push(ConstraintToMigrate {
2989 constraint_id,
2990 other_entity_id: ObjectId(other_id_usize),
2991 is_point_point: false,
2992 attach_to_endpoint: AttachToEndpoint::End,
2993 });
2994 }
2995 }
2996 }
2997 }
2998
2999 if let Some(end_id) = original_end_point_id {
3004 for obj in objects {
3005 let ObjectKind::Constraint { constraint } = &obj.kind else {
3006 continue;
3007 };
3008
3009 let Constraint::Coincident(coincident) = constraint else {
3010 continue;
3011 };
3012
3013 if !coincident.segments.contains(&trim_spawn_id) {
3018 continue;
3019 }
3020 if let (Some(start_id), Some(end_id_val)) = (original_start_point_id, Some(end_id))
3023 && coincident
3024 .segments
3025 .iter()
3026 .any(|id| *id == start_id || *id == end_id_val)
3027 {
3028 continue; }
3030
3031 let other_id = coincident
3033 .segments
3034 .iter()
3035 .find_map(|seg_id| if *seg_id != trim_spawn_id { Some(*seg_id) } else { None });
3036
3037 if let Some(other_id) = other_id {
3038 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3040 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3041 continue;
3042 };
3043
3044 let Segment::Point(point) = other_segment else {
3045 continue;
3046 };
3047
3048 let point_coords = Coords2d {
3050 x: number_to_unit(&point.position.x, default_unit),
3051 y: number_to_unit(&point.position.y, default_unit),
3052 };
3053
3054 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
3057 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
3058 if let ObjectKind::Segment {
3059 segment: Segment::Point(end_point),
3060 } = &end_point_obj.kind
3061 {
3062 Some(Coords2d {
3063 x: number_to_unit(&end_point.position.x, default_unit),
3064 y: number_to_unit(&end_point.position.y, default_unit),
3065 })
3066 } else {
3067 None
3068 }
3069 } else {
3070 None
3071 }
3072 } else {
3073 None
3074 };
3075
3076 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
3077 let dist_to_original_end = ((point_coords.x - reference_coords.x)
3078 * (point_coords.x - reference_coords.x)
3079 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
3080 .sqrt();
3081
3082 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3083 let has_point_point_constraint = find_point_point_coincident_constraints(end_id)
3086 .iter()
3087 .any(|&constraint_id| {
3088 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
3089 if let ObjectKind::Constraint {
3090 constraint: Constraint::Coincident(coincident),
3091 } = &constraint_obj.kind
3092 {
3093 coincident.segments.contains(&other_id)
3094 } else {
3095 false
3096 }
3097 } else {
3098 false
3099 }
3100 });
3101
3102 if !has_point_point_constraint {
3103 constraints_to_migrate.push(ConstraintToMigrate {
3105 constraint_id: obj.id,
3106 other_entity_id: other_id,
3107 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
3110 }
3111 constraints_to_delete_set.insert(obj.id);
3113 }
3114 }
3115 }
3116 }
3117 }
3118
3119 let split_point = right_trim_coords; let segment_start_coords = match segment {
3124 Segment::Line(_) => {
3125 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::Start, objects, default_unit)
3126 }
3127 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Start, objects, default_unit),
3128 _ => None,
3129 };
3130 let segment_end_coords = match segment {
3131 Segment::Line(_) => {
3132 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
3133 }
3134 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
3135 _ => None,
3136 };
3137 let segment_center_coords = match segment {
3138 Segment::Line(_) => None,
3139 Segment::Arc(_) => {
3140 get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Center, objects, default_unit)
3141 }
3142 _ => None,
3143 };
3144
3145 if let (Some(start_coords), Some(end_coords)) = (segment_start_coords, segment_end_coords) {
3146 let split_point_t_opt = match segment {
3148 Segment::Line(_) => Some(project_point_onto_segment(split_point, start_coords, end_coords)),
3149 Segment::Arc(_) => segment_center_coords
3150 .map(|center| project_point_onto_arc(split_point, center, start_coords, end_coords)),
3151 _ => None,
3152 };
3153
3154 if let Some(split_point_t) = split_point_t_opt {
3155 for obj in objects {
3157 let ObjectKind::Constraint { constraint } = &obj.kind else {
3158 continue;
3159 };
3160
3161 let Constraint::Coincident(coincident) = constraint else {
3162 continue;
3163 };
3164
3165 if !coincident.segments.contains(&trim_spawn_id) {
3167 continue;
3168 }
3169
3170 if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id)
3172 && coincident.segments.iter().any(|id| *id == start_id || *id == end_id)
3173 {
3174 continue;
3175 }
3176
3177 let other_id = coincident
3179 .segments
3180 .iter()
3181 .find_map(|seg_id| if *seg_id != trim_spawn_id { Some(*seg_id) } else { None });
3182
3183 if let Some(other_id) = other_id {
3184 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3186 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3187 continue;
3188 };
3189
3190 let Segment::Point(point) = other_segment else {
3191 continue;
3192 };
3193
3194 let point_coords = Coords2d {
3196 x: number_to_unit(&point.position.x, default_unit),
3197 y: number_to_unit(&point.position.y, default_unit),
3198 };
3199
3200 let point_t = match segment {
3202 Segment::Line(_) => project_point_onto_segment(point_coords, start_coords, end_coords),
3203 Segment::Arc(_) => {
3204 if let Some(center) = segment_center_coords {
3205 project_point_onto_arc(point_coords, center, start_coords, end_coords)
3206 } else {
3207 continue; }
3209 }
3210 _ => continue, };
3212
3213 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
3216 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
3217 if let ObjectKind::Segment {
3218 segment: Segment::Point(end_point),
3219 } = &end_point_obj.kind
3220 {
3221 Some(Coords2d {
3222 x: number_to_unit(&end_point.position.x, default_unit),
3223 y: number_to_unit(&end_point.position.y, default_unit),
3224 })
3225 } else {
3226 None
3227 }
3228 } else {
3229 None
3230 }
3231 } else {
3232 None
3233 };
3234
3235 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
3236 let dist_to_original_end = ((point_coords.x - reference_coords.x)
3237 * (point_coords.x - reference_coords.x)
3238 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
3239 .sqrt();
3240
3241 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3242 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
3246 find_point_point_coincident_constraints(end_id)
3247 .iter()
3248 .any(|&constraint_id| {
3249 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
3250 {
3251 if let ObjectKind::Constraint {
3252 constraint: Constraint::Coincident(coincident),
3253 } = &constraint_obj.kind
3254 {
3255 coincident.segments.contains(&other_id)
3256 } else {
3257 false
3258 }
3259 } else {
3260 false
3261 }
3262 })
3263 } else {
3264 false
3265 };
3266
3267 if !has_point_point_constraint {
3268 constraints_to_migrate.push(ConstraintToMigrate {
3270 constraint_id: obj.id,
3271 other_entity_id: other_id,
3272 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
3275 }
3276 constraints_to_delete_set.insert(obj.id);
3278 continue; }
3280
3281 let dist_to_start = ((point_coords.x - start_coords.x) * (point_coords.x - start_coords.x)
3283 + (point_coords.y - start_coords.y) * (point_coords.y - start_coords.y))
3284 .sqrt();
3285 let is_at_start = (point_t - 0.0).abs() < EPSILON_POINT_ON_SEGMENT
3286 || dist_to_start < EPSILON_POINT_ON_SEGMENT;
3287
3288 if is_at_start {
3289 continue; }
3291
3292 let dist_to_split = (point_t - split_point_t).abs();
3294 if dist_to_split < EPSILON_POINT_ON_SEGMENT * 100.0 {
3295 continue; }
3297
3298 if point_t > split_point_t {
3300 constraints_to_migrate.push(ConstraintToMigrate {
3301 constraint_id: obj.id,
3302 other_entity_id: other_id,
3303 is_point_point: false, attach_to_endpoint: AttachToEndpoint::Segment, });
3306 constraints_to_delete_set.insert(obj.id);
3307 }
3308 }
3309 }
3310 }
3311 } } let distance_constraint_ids_for_split = find_distance_constraints_for_segment(trim_spawn_id);
3319
3320 let arc_center_point_id: Option<ObjectId> = match segment {
3322 Segment::Arc(arc) => Some(arc.center),
3323 _ => None,
3324 };
3325
3326 for constraint_id in distance_constraint_ids_for_split {
3327 if let Some(center_id) = arc_center_point_id {
3329 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
3331 && let ObjectKind::Constraint { constraint } = &constraint_obj.kind
3332 && let Constraint::Distance(distance) = constraint
3333 && distance.points.contains(¢er_id)
3334 {
3335 continue;
3337 }
3338 }
3339
3340 constraints_to_delete_set.insert(constraint_id);
3341 }
3342
3343 for obj in objects {
3351 let ObjectKind::Constraint { constraint } = &obj.kind else {
3352 continue;
3353 };
3354
3355 let Constraint::Coincident(coincident) = constraint else {
3356 continue;
3357 };
3358
3359 if !coincident.segments.contains(&trim_spawn_id) {
3361 continue;
3362 }
3363
3364 if constraints_to_delete_set.contains(&obj.id) {
3366 continue;
3367 }
3368
3369 let other_id = coincident
3376 .segments
3377 .iter()
3378 .find_map(|seg_id| if *seg_id != trim_spawn_id { Some(*seg_id) } else { None });
3379
3380 if let Some(other_id) = other_id {
3381 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3383 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3384 continue;
3385 };
3386
3387 let Segment::Point(point) = other_segment else {
3388 continue;
3389 };
3390
3391 let _is_endpoint_constraint =
3394 if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id) {
3395 coincident.segments.iter().any(|id| *id == start_id || *id == end_id)
3396 } else {
3397 false
3398 };
3399
3400 let point_coords = Coords2d {
3402 x: number_to_unit(&point.position.x, default_unit),
3403 y: number_to_unit(&point.position.y, default_unit),
3404 };
3405
3406 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
3408 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
3409 if let ObjectKind::Segment {
3410 segment: Segment::Point(end_point),
3411 } = &end_point_obj.kind
3412 {
3413 Some(Coords2d {
3414 x: number_to_unit(&end_point.position.x, default_unit),
3415 y: number_to_unit(&end_point.position.y, default_unit),
3416 })
3417 } else {
3418 None
3419 }
3420 } else {
3421 None
3422 }
3423 } else {
3424 None
3425 };
3426
3427 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
3428 let dist_to_original_end = ((point_coords.x - reference_coords.x)
3429 * (point_coords.x - reference_coords.x)
3430 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
3431 .sqrt();
3432
3433 let is_at_original_end = dist_to_original_end < EPSILON_POINT_ON_SEGMENT * 2.0;
3436
3437 if is_at_original_end {
3438 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
3441 find_point_point_coincident_constraints(end_id)
3442 .iter()
3443 .any(|&constraint_id| {
3444 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
3445 if let ObjectKind::Constraint {
3446 constraint: Constraint::Coincident(coincident),
3447 } = &constraint_obj.kind
3448 {
3449 coincident.segments.contains(&other_id)
3450 } else {
3451 false
3452 }
3453 } else {
3454 false
3455 }
3456 })
3457 } else {
3458 false
3459 };
3460
3461 if !has_point_point_constraint {
3462 constraints_to_migrate.push(ConstraintToMigrate {
3464 constraint_id: obj.id,
3465 other_entity_id: other_id,
3466 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
3469 }
3470 constraints_to_delete_set.insert(obj.id);
3472 }
3473 }
3474 }
3475 }
3476
3477 let constraints_to_delete: Vec<ObjectId> = constraints_to_delete_set.iter().copied().collect();
3479 let operations = vec![TrimOperation::SplitSegment {
3480 segment_id: trim_spawn_id,
3481 left_trim_coords,
3482 right_trim_coords,
3483 original_end_coords,
3484 left_side: Box::new(left_side.clone()),
3485 right_side: Box::new(right_side.clone()),
3486 left_side_coincident_data: CoincidentData {
3487 intersecting_seg_id: left_intersecting_seg_id,
3488 intersecting_endpoint_point_id: left_coincident_data.intersecting_endpoint_point_id,
3489 existing_point_segment_constraint_id: left_coincident_data.existing_point_segment_constraint_id,
3490 },
3491 right_side_coincident_data: CoincidentData {
3492 intersecting_seg_id: right_intersecting_seg_id,
3493 intersecting_endpoint_point_id: right_coincident_data.intersecting_endpoint_point_id,
3494 existing_point_segment_constraint_id: right_coincident_data.existing_point_segment_constraint_id,
3495 },
3496 constraints_to_migrate,
3497 constraints_to_delete,
3498 }];
3499
3500 return Ok(operations);
3501 }
3502
3503 Err(format!(
3508 "Unsupported trim termination combination: left={:?} right={:?}",
3509 left_side, right_side
3510 ))
3511}
3512
3513pub(crate) async fn execute_trim_operations_simple(
3525 strategy: Vec<TrimOperation>,
3526 current_scene_graph_delta: &crate::frontend::api::SceneGraphDelta,
3527 frontend: &mut crate::frontend::FrontendState,
3528 ctx: &crate::ExecutorContext,
3529 version: crate::frontend::api::Version,
3530 sketch_id: ObjectId,
3531) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
3532 use crate::frontend::{
3533 SketchApi,
3534 sketch::{Constraint, ExistingSegmentCtor, SegmentCtor},
3535 };
3536
3537 let default_unit = frontend.default_length_unit();
3538
3539 let mut op_index = 0;
3540 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = None;
3541 let mut invalidates_ids = false;
3542
3543 while op_index < strategy.len() {
3544 let mut consumed_ops = 1;
3545 let operation_result = match &strategy[op_index] {
3546 TrimOperation::SimpleTrim { segment_to_trim_id } => {
3547 frontend
3549 .delete_objects(
3550 ctx,
3551 version,
3552 sketch_id,
3553 Vec::new(), vec![*segment_to_trim_id], )
3556 .await
3557 .map_err(|e| format!("Failed to delete segment: {}", e.msg))
3558 }
3559 TrimOperation::EditSegment {
3560 segment_id,
3561 ctor,
3562 endpoint_changed,
3563 } => {
3564 if op_index + 1 < strategy.len() {
3567 if let TrimOperation::AddCoincidentConstraint {
3568 segment_id: coincident_seg_id,
3569 endpoint_changed: coincident_endpoint_changed,
3570 segment_or_point_to_make_coincident_to,
3571 intersecting_endpoint_point_id,
3572 } = &strategy[op_index + 1]
3573 {
3574 if segment_id == coincident_seg_id && endpoint_changed == coincident_endpoint_changed {
3575 let mut delete_constraint_ids: Vec<ObjectId> = Vec::new();
3577 consumed_ops = 2;
3578
3579 if op_index + 2 < strategy.len()
3580 && let TrimOperation::DeleteConstraints { constraint_ids } = &strategy[op_index + 2]
3581 {
3582 delete_constraint_ids = constraint_ids.to_vec();
3583 consumed_ops = 3;
3584 }
3585
3586 let segment_ctor = ctor.clone();
3588
3589 let edited_segment = current_scene_graph_delta
3591 .new_graph
3592 .objects
3593 .iter()
3594 .find(|obj| obj.id == *segment_id)
3595 .ok_or_else(|| format!("Failed to find segment {} for tail-cut batch", segment_id.0))?;
3596
3597 let endpoint_point_id = match &edited_segment.kind {
3598 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
3599 crate::frontend::sketch::Segment::Line(line) => {
3600 if *endpoint_changed == EndpointChanged::Start {
3601 line.start
3602 } else {
3603 line.end
3604 }
3605 }
3606 crate::frontend::sketch::Segment::Arc(arc) => {
3607 if *endpoint_changed == EndpointChanged::Start {
3608 arc.start
3609 } else {
3610 arc.end
3611 }
3612 }
3613 _ => {
3614 return Err("Unsupported segment type for tail-cut batch".to_string());
3615 }
3616 },
3617 _ => {
3618 return Err("Edited object is not a segment (tail-cut batch)".to_string());
3619 }
3620 };
3621
3622 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
3623 vec![endpoint_point_id, *point_id]
3624 } else {
3625 vec![endpoint_point_id, *segment_or_point_to_make_coincident_to]
3626 };
3627
3628 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
3629 segments: coincident_segments,
3630 });
3631
3632 let segment_to_edit = ExistingSegmentCtor {
3633 id: *segment_id,
3634 ctor: segment_ctor,
3635 };
3636
3637 frontend
3640 .batch_tail_cut_operations(
3641 ctx,
3642 version,
3643 sketch_id,
3644 vec![segment_to_edit],
3645 vec![constraint],
3646 delete_constraint_ids,
3647 )
3648 .await
3649 .map_err(|e| format!("Failed to batch tail-cut operations: {}", e.msg))
3650 } else {
3651 let segment_to_edit = ExistingSegmentCtor {
3653 id: *segment_id,
3654 ctor: ctor.clone(),
3655 };
3656
3657 frontend
3658 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
3659 .await
3660 .map_err(|e| format!("Failed to edit segment: {}", e.msg))
3661 }
3662 } else {
3663 let segment_to_edit = ExistingSegmentCtor {
3665 id: *segment_id,
3666 ctor: ctor.clone(),
3667 };
3668
3669 frontend
3670 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
3671 .await
3672 .map_err(|e| format!("Failed to edit segment: {}", e.msg))
3673 }
3674 } else {
3675 let segment_to_edit = ExistingSegmentCtor {
3677 id: *segment_id,
3678 ctor: ctor.clone(),
3679 };
3680
3681 frontend
3682 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
3683 .await
3684 .map_err(|e| format!("Failed to edit segment: {}", e.msg))
3685 }
3686 }
3687 TrimOperation::AddCoincidentConstraint {
3688 segment_id,
3689 endpoint_changed,
3690 segment_or_point_to_make_coincident_to,
3691 intersecting_endpoint_point_id,
3692 } => {
3693 let edited_segment = current_scene_graph_delta
3695 .new_graph
3696 .objects
3697 .iter()
3698 .find(|obj| obj.id == *segment_id)
3699 .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
3700
3701 let new_segment_endpoint_point_id = match &edited_segment.kind {
3703 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
3704 crate::frontend::sketch::Segment::Line(line) => {
3705 if *endpoint_changed == EndpointChanged::Start {
3706 line.start
3707 } else {
3708 line.end
3709 }
3710 }
3711 crate::frontend::sketch::Segment::Arc(arc) => {
3712 if *endpoint_changed == EndpointChanged::Start {
3713 arc.start
3714 } else {
3715 arc.end
3716 }
3717 }
3718 _ => {
3719 return Err("Unsupported segment type for addCoincidentConstraint".to_string());
3720 }
3721 },
3722 _ => {
3723 return Err("Edited object is not a segment".to_string());
3724 }
3725 };
3726
3727 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
3729 vec![new_segment_endpoint_point_id, *point_id]
3730 } else {
3731 vec![new_segment_endpoint_point_id, *segment_or_point_to_make_coincident_to]
3732 };
3733
3734 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
3735 segments: coincident_segments,
3736 });
3737
3738 frontend
3739 .add_constraint(ctx, version, sketch_id, constraint)
3740 .await
3741 .map_err(|e| format!("Failed to add constraint: {}", e.msg))
3742 }
3743 TrimOperation::DeleteConstraints { constraint_ids } => {
3744 let constraint_object_ids: Vec<ObjectId> = constraint_ids.to_vec();
3746
3747 frontend
3748 .delete_objects(
3749 ctx,
3750 version,
3751 sketch_id,
3752 constraint_object_ids,
3753 Vec::new(), )
3755 .await
3756 .map_err(|e| format!("Failed to delete constraints: {}", e.msg))
3757 }
3758 TrimOperation::SplitSegment {
3759 segment_id,
3760 left_trim_coords,
3761 right_trim_coords,
3762 original_end_coords,
3763 left_side,
3764 right_side,
3765 constraints_to_migrate,
3766 constraints_to_delete,
3767 ..
3768 } => {
3769 let original_segment = current_scene_graph_delta
3774 .new_graph
3775 .objects
3776 .iter()
3777 .find(|obj| obj.id == *segment_id)
3778 .ok_or_else(|| format!("Failed to find original segment {}", segment_id.0))?;
3779
3780 let (original_segment_start_point_id, original_segment_end_point_id, original_segment_center_point_id) =
3782 match &original_segment.kind {
3783 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
3784 crate::frontend::sketch::Segment::Line(line) => (Some(line.start), Some(line.end), None),
3785 crate::frontend::sketch::Segment::Arc(arc) => {
3786 (Some(arc.start), Some(arc.end), Some(arc.center))
3787 }
3788 _ => (None, None, None),
3789 },
3790 _ => (None, None, None),
3791 };
3792
3793 let mut center_point_constraints_to_migrate: Vec<(Constraint, ObjectId)> = Vec::new();
3795 if let Some(original_center_id) = original_segment_center_point_id {
3796 for obj in ¤t_scene_graph_delta.new_graph.objects {
3797 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
3798 continue;
3799 };
3800
3801 if let Constraint::Coincident(coincident) = constraint
3803 && coincident.segments.contains(&original_center_id)
3804 {
3805 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
3806 }
3807
3808 if let Constraint::Distance(distance) = constraint
3810 && distance.points.contains(&original_center_id)
3811 {
3812 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
3813 }
3814 }
3815 }
3816
3817 let (_segment_type, original_ctor) = match &original_segment.kind {
3819 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
3820 crate::frontend::sketch::Segment::Line(line) => ("Line", line.ctor.clone()),
3821 crate::frontend::sketch::Segment::Arc(arc) => ("Arc", arc.ctor.clone()),
3822 _ => {
3823 return Err("Original segment is not a Line or Arc".to_string());
3824 }
3825 },
3826 _ => {
3827 return Err("Original object is not a segment".to_string());
3828 }
3829 };
3830
3831 let units = match &original_ctor {
3833 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
3834 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
3835 _ => crate::pretty::NumericSuffix::Mm,
3836 },
3837 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
3838 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
3839 _ => crate::pretty::NumericSuffix::Mm,
3840 },
3841 _ => crate::pretty::NumericSuffix::Mm,
3842 };
3843
3844 let coords_to_point =
3847 |coords: Coords2d| -> crate::frontend::sketch::Point2d<crate::frontend::api::Number> {
3848 crate::frontend::sketch::Point2d {
3849 x: unit_to_number(coords.x, default_unit, units),
3850 y: unit_to_number(coords.y, default_unit, units),
3851 }
3852 };
3853
3854 let point_to_expr = |point: crate::frontend::sketch::Point2d<crate::frontend::api::Number>| -> crate::frontend::sketch::Point2d<crate::frontend::api::Expr> {
3856 crate::frontend::sketch::Point2d {
3857 x: crate::frontend::api::Expr::Var(point.x),
3858 y: crate::frontend::api::Expr::Var(point.y),
3859 }
3860 };
3861
3862 let new_segment_ctor = match &original_ctor {
3864 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3865 start: point_to_expr(coords_to_point(*right_trim_coords)),
3866 end: point_to_expr(coords_to_point(*original_end_coords)),
3867 construction: line_ctor.construction,
3868 }),
3869 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3870 start: point_to_expr(coords_to_point(*right_trim_coords)),
3871 end: point_to_expr(coords_to_point(*original_end_coords)),
3872 center: arc_ctor.center.clone(),
3873 construction: arc_ctor.construction,
3874 }),
3875 _ => {
3876 return Err("Unsupported segment type for new segment".to_string());
3877 }
3878 };
3879
3880 let (_add_source_delta, add_scene_graph_delta) = frontend
3881 .add_segment(ctx, version, sketch_id, new_segment_ctor, None)
3882 .await
3883 .map_err(|e| format!("Failed to add new segment: {}", e.msg))?;
3884
3885 let new_segment_id = *add_scene_graph_delta
3887 .new_objects
3888 .iter()
3889 .find(|&id| {
3890 if let Some(obj) = add_scene_graph_delta.new_graph.objects.iter().find(|o| o.id == *id) {
3891 matches!(
3892 &obj.kind,
3893 crate::frontend::api::ObjectKind::Segment { segment }
3894 if matches!(segment, crate::frontend::sketch::Segment::Line(_) | crate::frontend::sketch::Segment::Arc(_))
3895 )
3896 } else {
3897 false
3898 }
3899 })
3900 .ok_or_else(|| "Failed to find newly created segment".to_string())?;
3901
3902 let new_segment = add_scene_graph_delta
3903 .new_graph
3904 .objects
3905 .iter()
3906 .find(|o| o.id == new_segment_id)
3907 .ok_or_else(|| format!("New segment not found with id {}", new_segment_id.0))?;
3908
3909 let (new_segment_start_point_id, new_segment_end_point_id, new_segment_center_point_id) =
3911 match &new_segment.kind {
3912 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
3913 crate::frontend::sketch::Segment::Line(line) => (line.start, line.end, None),
3914 crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, Some(arc.center)),
3915 _ => {
3916 return Err("New segment is not a Line or Arc".to_string());
3917 }
3918 },
3919 _ => {
3920 return Err("New segment is not a segment".to_string());
3921 }
3922 };
3923
3924 let edited_ctor = match &original_ctor {
3926 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3927 start: line_ctor.start.clone(),
3928 end: point_to_expr(coords_to_point(*left_trim_coords)),
3929 construction: line_ctor.construction,
3930 }),
3931 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3932 start: arc_ctor.start.clone(),
3933 end: point_to_expr(coords_to_point(*left_trim_coords)),
3934 center: arc_ctor.center.clone(),
3935 construction: arc_ctor.construction,
3936 }),
3937 _ => {
3938 return Err("Unsupported segment type for split".to_string());
3939 }
3940 };
3941
3942 let (_edit_source_delta, edit_scene_graph_delta) = frontend
3943 .edit_segments(
3944 ctx,
3945 version,
3946 sketch_id,
3947 vec![ExistingSegmentCtor {
3948 id: *segment_id,
3949 ctor: edited_ctor,
3950 }],
3951 )
3952 .await
3953 .map_err(|e| format!("Failed to edit segment: {}", e.msg))?;
3954 invalidates_ids = invalidates_ids || edit_scene_graph_delta.invalidates_ids;
3956
3957 let edited_segment = edit_scene_graph_delta
3959 .new_graph
3960 .objects
3961 .iter()
3962 .find(|obj| obj.id == *segment_id)
3963 .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
3964
3965 let left_side_endpoint_point_id = match &edited_segment.kind {
3966 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
3967 crate::frontend::sketch::Segment::Line(line) => line.end,
3968 crate::frontend::sketch::Segment::Arc(arc) => arc.end,
3969 _ => {
3970 return Err("Edited segment is not a Line or Arc".to_string());
3971 }
3972 },
3973 _ => {
3974 return Err("Edited segment is not a segment".to_string());
3975 }
3976 };
3977
3978 let mut batch_constraints = Vec::new();
3980
3981 let left_intersecting_seg_id = match &**left_side {
3983 TrimTermination::Intersection {
3984 intersecting_seg_id, ..
3985 }
3986 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3987 intersecting_seg_id, ..
3988 } => *intersecting_seg_id,
3989 _ => {
3990 return Err("Left side is not an intersection or coincident".to_string());
3991 }
3992 };
3993 let left_coincident_segments = match &**left_side {
3994 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3995 other_segment_point_id,
3996 ..
3997 } => {
3998 vec![left_side_endpoint_point_id, *other_segment_point_id]
3999 }
4000 _ => {
4001 vec![left_side_endpoint_point_id, left_intersecting_seg_id]
4002 }
4003 };
4004 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
4005 segments: left_coincident_segments,
4006 }));
4007
4008 let right_intersecting_seg_id = match &**right_side {
4010 TrimTermination::Intersection {
4011 intersecting_seg_id, ..
4012 }
4013 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4014 intersecting_seg_id, ..
4015 } => *intersecting_seg_id,
4016 _ => {
4017 return Err("Right side is not an intersection or coincident".to_string());
4018 }
4019 };
4020
4021 let mut intersection_point_id: Option<ObjectId> = None;
4022 if matches!(&**right_side, TrimTermination::Intersection { .. }) {
4023 let intersecting_seg = edit_scene_graph_delta
4024 .new_graph
4025 .objects
4026 .iter()
4027 .find(|obj| obj.id == right_intersecting_seg_id);
4028
4029 if let Some(seg) = intersecting_seg {
4030 let endpoint_epsilon = 1e-3; let right_trim_coords_value = *right_trim_coords;
4032
4033 if let crate::frontend::api::ObjectKind::Segment { segment } = &seg.kind {
4034 match segment {
4035 crate::frontend::sketch::Segment::Line(_) => {
4036 if let (Some(start_coords), Some(end_coords)) = (
4037 crate::frontend::trim::get_position_coords_for_line(
4038 seg,
4039 crate::frontend::trim::LineEndpoint::Start,
4040 &edit_scene_graph_delta.new_graph.objects,
4041 default_unit,
4042 ),
4043 crate::frontend::trim::get_position_coords_for_line(
4044 seg,
4045 crate::frontend::trim::LineEndpoint::End,
4046 &edit_scene_graph_delta.new_graph.objects,
4047 default_unit,
4048 ),
4049 ) {
4050 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
4051 * (right_trim_coords_value.x - start_coords.x)
4052 + (right_trim_coords_value.y - start_coords.y)
4053 * (right_trim_coords_value.y - start_coords.y))
4054 .sqrt();
4055 if dist_to_start < endpoint_epsilon {
4056 if let crate::frontend::sketch::Segment::Line(line) = segment {
4057 intersection_point_id = Some(line.start);
4058 }
4059 } else {
4060 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
4061 * (right_trim_coords_value.x - end_coords.x)
4062 + (right_trim_coords_value.y - end_coords.y)
4063 * (right_trim_coords_value.y - end_coords.y))
4064 .sqrt();
4065 if dist_to_end < endpoint_epsilon
4066 && let crate::frontend::sketch::Segment::Line(line) = segment
4067 {
4068 intersection_point_id = Some(line.end);
4069 }
4070 }
4071 }
4072 }
4073 crate::frontend::sketch::Segment::Arc(_) => {
4074 if let (Some(start_coords), Some(end_coords)) = (
4075 crate::frontend::trim::get_position_coords_from_arc(
4076 seg,
4077 crate::frontend::trim::ArcPoint::Start,
4078 &edit_scene_graph_delta.new_graph.objects,
4079 default_unit,
4080 ),
4081 crate::frontend::trim::get_position_coords_from_arc(
4082 seg,
4083 crate::frontend::trim::ArcPoint::End,
4084 &edit_scene_graph_delta.new_graph.objects,
4085 default_unit,
4086 ),
4087 ) {
4088 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
4089 * (right_trim_coords_value.x - start_coords.x)
4090 + (right_trim_coords_value.y - start_coords.y)
4091 * (right_trim_coords_value.y - start_coords.y))
4092 .sqrt();
4093 if dist_to_start < endpoint_epsilon {
4094 if let crate::frontend::sketch::Segment::Arc(arc) = segment {
4095 intersection_point_id = Some(arc.start);
4096 }
4097 } else {
4098 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
4099 * (right_trim_coords_value.x - end_coords.x)
4100 + (right_trim_coords_value.y - end_coords.y)
4101 * (right_trim_coords_value.y - end_coords.y))
4102 .sqrt();
4103 if dist_to_end < endpoint_epsilon
4104 && let crate::frontend::sketch::Segment::Arc(arc) = segment
4105 {
4106 intersection_point_id = Some(arc.end);
4107 }
4108 }
4109 }
4110 }
4111 _ => {}
4112 }
4113 }
4114 }
4115 }
4116
4117 let right_coincident_segments = if let Some(point_id) = intersection_point_id {
4118 vec![new_segment_start_point_id, point_id]
4119 } else if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4120 other_segment_point_id,
4121 ..
4122 } = &**right_side
4123 {
4124 vec![new_segment_start_point_id, *other_segment_point_id]
4125 } else {
4126 vec![new_segment_start_point_id, right_intersecting_seg_id]
4127 };
4128 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
4129 segments: right_coincident_segments,
4130 }));
4131
4132 let mut points_constrained_to_new_segment_start = std::collections::HashSet::new();
4134 let mut points_constrained_to_new_segment_end = std::collections::HashSet::new();
4135
4136 if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4137 other_segment_point_id,
4138 ..
4139 } = &**right_side
4140 {
4141 points_constrained_to_new_segment_start.insert(other_segment_point_id);
4142 }
4143
4144 for constraint_to_migrate in constraints_to_migrate.iter() {
4145 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::End
4146 && constraint_to_migrate.is_point_point
4147 {
4148 points_constrained_to_new_segment_end.insert(constraint_to_migrate.other_entity_id);
4149 }
4150 }
4151
4152 for constraint_to_migrate in constraints_to_migrate.iter() {
4153 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment
4155 && (points_constrained_to_new_segment_start.contains(&constraint_to_migrate.other_entity_id)
4156 || points_constrained_to_new_segment_end.contains(&constraint_to_migrate.other_entity_id))
4157 {
4158 continue; }
4160
4161 let constraint_segments = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment {
4162 vec![constraint_to_migrate.other_entity_id, new_segment_id]
4163 } else {
4164 let target_endpoint_id = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Start
4165 {
4166 new_segment_start_point_id
4167 } else {
4168 new_segment_end_point_id
4169 };
4170 vec![target_endpoint_id, constraint_to_migrate.other_entity_id]
4171 };
4172 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
4173 segments: constraint_segments,
4174 }));
4175 }
4176
4177 let mut distance_constraints_to_re_add: Vec<(
4179 crate::frontend::api::Number,
4180 crate::frontend::sketch::ConstraintSource,
4181 )> = Vec::new();
4182 if let (Some(original_start_id), Some(original_end_id)) =
4183 (original_segment_start_point_id, original_segment_end_point_id)
4184 {
4185 for obj in &edit_scene_graph_delta.new_graph.objects {
4186 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4187 continue;
4188 };
4189
4190 let Constraint::Distance(distance) = constraint else {
4191 continue;
4192 };
4193
4194 let references_start = distance.points.contains(&original_start_id);
4195 let references_end = distance.points.contains(&original_end_id);
4196
4197 if references_start && references_end {
4198 distance_constraints_to_re_add.push((distance.distance, distance.source.clone()));
4199 }
4200 }
4201 }
4202
4203 if let Some(original_start_id) = original_segment_start_point_id {
4205 for (distance_value, source) in distance_constraints_to_re_add {
4206 batch_constraints.push(Constraint::Distance(crate::frontend::sketch::Distance {
4207 points: vec![original_start_id, new_segment_end_point_id],
4208 distance: distance_value,
4209 source,
4210 }));
4211 }
4212 }
4213
4214 if let Some(new_center_id) = new_segment_center_point_id {
4216 for (constraint, original_center_id) in center_point_constraints_to_migrate {
4217 match constraint {
4218 Constraint::Coincident(coincident) => {
4219 let new_segments: Vec<ObjectId> = coincident
4220 .segments
4221 .iter()
4222 .map(|seg_id| {
4223 if *seg_id == original_center_id {
4224 new_center_id
4225 } else {
4226 *seg_id
4227 }
4228 })
4229 .collect();
4230
4231 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
4232 segments: new_segments,
4233 }));
4234 }
4235 Constraint::Distance(distance) => {
4236 let new_points: Vec<ObjectId> = distance
4237 .points
4238 .iter()
4239 .map(|pt| if *pt == original_center_id { new_center_id } else { *pt })
4240 .collect();
4241
4242 batch_constraints.push(Constraint::Distance(crate::frontend::sketch::Distance {
4243 points: new_points,
4244 distance: distance.distance,
4245 source: distance.source.clone(),
4246 }));
4247 }
4248 _ => {}
4249 }
4250 }
4251 }
4252
4253 for obj in &edit_scene_graph_delta.new_graph.objects {
4255 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4256 continue;
4257 };
4258
4259 let should_migrate = match constraint {
4260 Constraint::Parallel(parallel) => parallel.lines.contains(segment_id),
4261 Constraint::Perpendicular(perpendicular) => perpendicular.lines.contains(segment_id),
4262 Constraint::Horizontal(horizontal) => horizontal.line == *segment_id,
4263 Constraint::Vertical(vertical) => vertical.line == *segment_id,
4264 _ => false,
4265 };
4266
4267 if should_migrate {
4268 let migrated_constraint = match constraint {
4269 Constraint::Parallel(parallel) => {
4270 let new_lines: Vec<ObjectId> = parallel
4271 .lines
4272 .iter()
4273 .map(|line_id| {
4274 if *line_id == *segment_id {
4275 new_segment_id
4276 } else {
4277 *line_id
4278 }
4279 })
4280 .collect();
4281 Constraint::Parallel(crate::frontend::sketch::Parallel { lines: new_lines })
4282 }
4283 Constraint::Perpendicular(perpendicular) => {
4284 let new_lines: Vec<ObjectId> = perpendicular
4285 .lines
4286 .iter()
4287 .map(|line_id| {
4288 if *line_id == *segment_id {
4289 new_segment_id
4290 } else {
4291 *line_id
4292 }
4293 })
4294 .collect();
4295 Constraint::Perpendicular(crate::frontend::sketch::Perpendicular { lines: new_lines })
4296 }
4297 Constraint::Horizontal(horizontal) => {
4298 if horizontal.line == *segment_id {
4299 Constraint::Horizontal(crate::frontend::sketch::Horizontal { line: new_segment_id })
4300 } else {
4301 continue;
4302 }
4303 }
4304 Constraint::Vertical(vertical) => {
4305 if vertical.line == *segment_id {
4306 Constraint::Vertical(crate::frontend::sketch::Vertical { line: new_segment_id })
4307 } else {
4308 continue;
4309 }
4310 }
4311 _ => continue,
4312 };
4313 batch_constraints.push(migrated_constraint);
4314 }
4315 }
4316
4317 let constraint_object_ids: Vec<ObjectId> = constraints_to_delete.to_vec();
4319
4320 let batch_result = frontend
4321 .batch_split_segment_operations(
4322 ctx,
4323 version,
4324 sketch_id,
4325 Vec::new(), batch_constraints,
4327 constraint_object_ids,
4328 crate::frontend::sketch::NewSegmentInfo {
4329 segment_id: new_segment_id,
4330 start_point_id: new_segment_start_point_id,
4331 end_point_id: new_segment_end_point_id,
4332 center_point_id: new_segment_center_point_id,
4333 },
4334 )
4335 .await
4336 .map_err(|e| format!("Failed to batch split segment operations: {}", e.msg));
4337 if let Ok((_, ref batch_delta)) = batch_result {
4339 invalidates_ids = invalidates_ids || batch_delta.invalidates_ids;
4340 }
4341 batch_result
4342 }
4343 };
4344
4345 match operation_result {
4346 Ok((source_delta, scene_graph_delta)) => {
4347 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
4349 last_result = Some((source_delta, scene_graph_delta.clone()));
4350 }
4351 Err(e) => {
4352 crate::logln!("Error executing trim operation {}: {}", op_index, e);
4353 }
4355 }
4356
4357 op_index += consumed_ops;
4358 }
4359
4360 let (source_delta, mut scene_graph_delta) =
4361 last_result.ok_or_else(|| "No operations were executed successfully".to_string())?;
4362 scene_graph_delta.invalidates_ids = invalidates_ids;
4364 Ok((source_delta, scene_graph_delta))
4365}