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