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 CirclePoint {
86 Start,
87 Center,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum TrimDirection {
93 Left,
94 Right,
95}
96
97#[derive(Debug, Clone)]
105pub enum TrimItem {
106 Spawn {
107 trim_spawn_seg_id: ObjectId,
108 trim_spawn_coords: Coords2d,
109 next_index: usize,
110 },
111 None {
112 next_index: usize,
113 },
114}
115
116#[derive(Debug, Clone)]
123pub enum TrimTermination {
124 SegEndPoint {
125 trim_termination_coords: Coords2d,
126 },
127 Intersection {
128 trim_termination_coords: Coords2d,
129 intersecting_seg_id: ObjectId,
130 },
131 TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
132 trim_termination_coords: Coords2d,
133 intersecting_seg_id: ObjectId,
134 other_segment_point_id: ObjectId,
135 },
136}
137
138#[derive(Debug, Clone)]
140pub struct TrimTerminations {
141 pub left_side: TrimTermination,
142 pub right_side: TrimTermination,
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum AttachToEndpoint {
148 Start,
149 End,
150 Segment,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub enum EndpointChanged {
156 Start,
157 End,
158}
159
160#[derive(Debug, Clone)]
162pub struct CoincidentData {
163 pub intersecting_seg_id: ObjectId,
164 pub intersecting_endpoint_point_id: Option<ObjectId>,
165 pub existing_point_segment_constraint_id: Option<ObjectId>,
166}
167
168#[derive(Debug, Clone)]
170pub struct ConstraintToMigrate {
171 pub constraint_id: ObjectId,
172 pub other_entity_id: ObjectId,
173 pub is_point_point: bool,
176 pub attach_to_endpoint: AttachToEndpoint,
177}
178
179#[derive(Debug, Clone)]
181pub enum TrimPlan {
182 DeleteSegment {
183 segment_id: ObjectId,
184 },
185 TailCut {
186 segment_id: ObjectId,
187 endpoint_changed: EndpointChanged,
188 ctor: SegmentCtor,
189 segment_or_point_to_make_coincident_to: ObjectId,
190 intersecting_endpoint_point_id: Option<ObjectId>,
191 constraint_ids_to_delete: Vec<ObjectId>,
192 },
193 ReplaceCircleWithArc {
194 circle_id: ObjectId,
195 arc_start_coords: Coords2d,
196 arc_end_coords: Coords2d,
197 arc_start_termination: Box<TrimTermination>,
198 arc_end_termination: Box<TrimTermination>,
199 },
200 SplitSegment {
201 segment_id: ObjectId,
202 left_trim_coords: Coords2d,
203 right_trim_coords: Coords2d,
204 original_end_coords: Coords2d,
205 left_side: Box<TrimTermination>,
206 right_side: Box<TrimTermination>,
207 left_side_coincident_data: CoincidentData,
208 right_side_coincident_data: CoincidentData,
209 constraints_to_migrate: Vec<ConstraintToMigrate>,
210 constraints_to_delete: Vec<ObjectId>,
211 },
212}
213
214fn lower_trim_plan(plan: &TrimPlan) -> Vec<TrimOperation> {
215 match plan {
216 TrimPlan::DeleteSegment { segment_id } => vec![TrimOperation::SimpleTrim {
217 segment_to_trim_id: *segment_id,
218 }],
219 TrimPlan::TailCut {
220 segment_id,
221 endpoint_changed,
222 ctor,
223 segment_or_point_to_make_coincident_to,
224 intersecting_endpoint_point_id,
225 constraint_ids_to_delete,
226 } => {
227 let mut ops = vec![
228 TrimOperation::EditSegment {
229 segment_id: *segment_id,
230 ctor: ctor.clone(),
231 endpoint_changed: *endpoint_changed,
232 },
233 TrimOperation::AddCoincidentConstraint {
234 segment_id: *segment_id,
235 endpoint_changed: *endpoint_changed,
236 segment_or_point_to_make_coincident_to: *segment_or_point_to_make_coincident_to,
237 intersecting_endpoint_point_id: *intersecting_endpoint_point_id,
238 },
239 ];
240 if !constraint_ids_to_delete.is_empty() {
241 ops.push(TrimOperation::DeleteConstraints {
242 constraint_ids: constraint_ids_to_delete.clone(),
243 });
244 }
245 ops
246 }
247 TrimPlan::ReplaceCircleWithArc {
248 circle_id,
249 arc_start_coords,
250 arc_end_coords,
251 arc_start_termination,
252 arc_end_termination,
253 } => vec![TrimOperation::ReplaceCircleWithArc {
254 circle_id: *circle_id,
255 arc_start_coords: *arc_start_coords,
256 arc_end_coords: *arc_end_coords,
257 arc_start_termination: arc_start_termination.clone(),
258 arc_end_termination: arc_end_termination.clone(),
259 }],
260 TrimPlan::SplitSegment {
261 segment_id,
262 left_trim_coords,
263 right_trim_coords,
264 original_end_coords,
265 left_side,
266 right_side,
267 left_side_coincident_data,
268 right_side_coincident_data,
269 constraints_to_migrate,
270 constraints_to_delete,
271 } => vec![TrimOperation::SplitSegment {
272 segment_id: *segment_id,
273 left_trim_coords: *left_trim_coords,
274 right_trim_coords: *right_trim_coords,
275 original_end_coords: *original_end_coords,
276 left_side: left_side.clone(),
277 right_side: right_side.clone(),
278 left_side_coincident_data: left_side_coincident_data.clone(),
279 right_side_coincident_data: right_side_coincident_data.clone(),
280 constraints_to_migrate: constraints_to_migrate.clone(),
281 constraints_to_delete: constraints_to_delete.clone(),
282 }],
283 }
284}
285
286fn trim_plan_modifies_geometry(plan: &TrimPlan) -> bool {
287 matches!(
288 plan,
289 TrimPlan::DeleteSegment { .. }
290 | TrimPlan::TailCut { .. }
291 | TrimPlan::ReplaceCircleWithArc { .. }
292 | TrimPlan::SplitSegment { .. }
293 )
294}
295
296fn rewrite_object_id(id: ObjectId, rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>) -> ObjectId {
297 rewrite_map.get(&id).copied().unwrap_or(id)
298}
299
300fn rewrite_constraint_segment(
301 segment: crate::frontend::sketch::ConstraintSegment,
302 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
303) -> crate::frontend::sketch::ConstraintSegment {
304 match segment {
305 crate::frontend::sketch::ConstraintSegment::Segment(id) => {
306 crate::frontend::sketch::ConstraintSegment::Segment(rewrite_object_id(id, rewrite_map))
307 }
308 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
309 crate::frontend::sketch::ConstraintSegment::Origin(origin)
310 }
311 }
312}
313
314fn rewrite_constraint_segments(
315 segments: &[crate::frontend::sketch::ConstraintSegment],
316 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
317) -> Vec<crate::frontend::sketch::ConstraintSegment> {
318 segments
319 .iter()
320 .copied()
321 .map(|segment| rewrite_constraint_segment(segment, rewrite_map))
322 .collect()
323}
324
325fn constraint_segments_reference_any(
326 segments: &[crate::frontend::sketch::ConstraintSegment],
327 ids: &std::collections::HashSet<ObjectId>,
328) -> bool {
329 segments.iter().any(|segment| match segment {
330 crate::frontend::sketch::ConstraintSegment::Segment(id) => ids.contains(id),
331 crate::frontend::sketch::ConstraintSegment::Origin(_) => false,
332 })
333}
334
335fn rewrite_constraint_with_map(
336 constraint: &Constraint,
337 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
338) -> Option<Constraint> {
339 match constraint {
340 Constraint::Coincident(coincident) => Some(Constraint::Coincident(crate::frontend::sketch::Coincident {
341 segments: rewrite_constraint_segments(&coincident.segments, rewrite_map),
342 })),
343 Constraint::Distance(distance) => Some(Constraint::Distance(crate::frontend::sketch::Distance {
344 points: rewrite_constraint_segments(&distance.points, rewrite_map),
345 distance: distance.distance,
346 source: distance.source.clone(),
347 })),
348 Constraint::HorizontalDistance(distance) => {
349 Some(Constraint::HorizontalDistance(crate::frontend::sketch::Distance {
350 points: rewrite_constraint_segments(&distance.points, rewrite_map),
351 distance: distance.distance,
352 source: distance.source.clone(),
353 }))
354 }
355 Constraint::VerticalDistance(distance) => {
356 Some(Constraint::VerticalDistance(crate::frontend::sketch::Distance {
357 points: rewrite_constraint_segments(&distance.points, rewrite_map),
358 distance: distance.distance,
359 source: distance.source.clone(),
360 }))
361 }
362 Constraint::Radius(radius) => Some(Constraint::Radius(crate::frontend::sketch::Radius {
363 arc: rewrite_object_id(radius.arc, rewrite_map),
364 radius: radius.radius,
365 source: radius.source.clone(),
366 })),
367 Constraint::Diameter(diameter) => Some(Constraint::Diameter(crate::frontend::sketch::Diameter {
368 arc: rewrite_object_id(diameter.arc, rewrite_map),
369 diameter: diameter.diameter,
370 source: diameter.source.clone(),
371 })),
372 Constraint::EqualRadius(equal_radius) => Some(Constraint::EqualRadius(crate::frontend::sketch::EqualRadius {
373 input: equal_radius
374 .input
375 .iter()
376 .map(|id| rewrite_object_id(*id, rewrite_map))
377 .collect(),
378 })),
379 Constraint::Tangent(tangent) => Some(Constraint::Tangent(crate::frontend::sketch::Tangent {
380 input: tangent
381 .input
382 .iter()
383 .map(|id| rewrite_object_id(*id, rewrite_map))
384 .collect(),
385 })),
386 Constraint::Parallel(parallel) => Some(Constraint::Parallel(crate::frontend::sketch::Parallel {
387 lines: parallel
388 .lines
389 .iter()
390 .map(|id| rewrite_object_id(*id, rewrite_map))
391 .collect(),
392 })),
393 Constraint::Perpendicular(perpendicular) => {
394 Some(Constraint::Perpendicular(crate::frontend::sketch::Perpendicular {
395 lines: perpendicular
396 .lines
397 .iter()
398 .map(|id| rewrite_object_id(*id, rewrite_map))
399 .collect(),
400 }))
401 }
402 Constraint::Horizontal(horizontal) => Some(Constraint::Horizontal(crate::frontend::sketch::Horizontal {
403 line: rewrite_object_id(horizontal.line, rewrite_map),
404 })),
405 Constraint::Vertical(vertical) => Some(Constraint::Vertical(crate::frontend::sketch::Vertical {
406 line: rewrite_object_id(vertical.line, rewrite_map),
407 })),
408 _ => None,
409 }
410}
411
412#[derive(Debug, Clone)]
413#[allow(clippy::large_enum_variant)]
414pub enum TrimOperation {
415 SimpleTrim {
416 segment_to_trim_id: ObjectId,
417 },
418 EditSegment {
419 segment_id: ObjectId,
420 ctor: SegmentCtor,
421 endpoint_changed: EndpointChanged,
422 },
423 AddCoincidentConstraint {
424 segment_id: ObjectId,
425 endpoint_changed: EndpointChanged,
426 segment_or_point_to_make_coincident_to: ObjectId,
427 intersecting_endpoint_point_id: Option<ObjectId>,
428 },
429 SplitSegment {
430 segment_id: ObjectId,
431 left_trim_coords: Coords2d,
432 right_trim_coords: Coords2d,
433 original_end_coords: Coords2d,
434 left_side: Box<TrimTermination>,
435 right_side: Box<TrimTermination>,
436 left_side_coincident_data: CoincidentData,
437 right_side_coincident_data: CoincidentData,
438 constraints_to_migrate: Vec<ConstraintToMigrate>,
439 constraints_to_delete: Vec<ObjectId>,
440 },
441 ReplaceCircleWithArc {
442 circle_id: ObjectId,
443 arc_start_coords: Coords2d,
444 arc_end_coords: Coords2d,
445 arc_start_termination: Box<TrimTermination>,
446 arc_end_termination: Box<TrimTermination>,
447 },
448 DeleteConstraints {
449 constraint_ids: Vec<ObjectId>,
450 },
451}
452
453pub fn is_point_on_line_segment(
457 point: Coords2d,
458 segment_start: Coords2d,
459 segment_end: Coords2d,
460 epsilon: f64,
461) -> Option<Coords2d> {
462 let dx = segment_end.x - segment_start.x;
463 let dy = segment_end.y - segment_start.y;
464 let segment_length_sq = dx * dx + dy * dy;
465
466 if segment_length_sq < EPSILON_PARALLEL {
467 let dist_sq = (point.x - segment_start.x) * (point.x - segment_start.x)
469 + (point.y - segment_start.y) * (point.y - segment_start.y);
470 if dist_sq <= epsilon * epsilon {
471 return Some(point);
472 }
473 return None;
474 }
475
476 let point_dx = point.x - segment_start.x;
477 let point_dy = point.y - segment_start.y;
478 let projection_param = (point_dx * dx + point_dy * dy) / segment_length_sq;
479
480 if !(0.0..=1.0).contains(&projection_param) {
482 return None;
483 }
484
485 let projected_point = Coords2d {
487 x: segment_start.x + projection_param * dx,
488 y: segment_start.y + projection_param * dy,
489 };
490
491 let dist_dx = point.x - projected_point.x;
493 let dist_dy = point.y - projected_point.y;
494 let distance_sq = dist_dx * dist_dx + dist_dy * dist_dy;
495
496 if distance_sq <= epsilon * epsilon {
497 Some(point)
498 } else {
499 None
500 }
501}
502
503pub fn line_segment_intersection(
507 line1_start: Coords2d,
508 line1_end: Coords2d,
509 line2_start: Coords2d,
510 line2_end: Coords2d,
511 epsilon: f64,
512) -> Option<Coords2d> {
513 if let Some(point) = is_point_on_line_segment(line1_start, line2_start, line2_end, epsilon) {
515 return Some(point);
516 }
517
518 if let Some(point) = is_point_on_line_segment(line1_end, line2_start, line2_end, epsilon) {
519 return Some(point);
520 }
521
522 if let Some(point) = is_point_on_line_segment(line2_start, line1_start, line1_end, epsilon) {
523 return Some(point);
524 }
525
526 if let Some(point) = is_point_on_line_segment(line2_end, line1_start, line1_end, epsilon) {
527 return Some(point);
528 }
529
530 let x1 = line1_start.x;
532 let y1 = line1_start.y;
533 let x2 = line1_end.x;
534 let y2 = line1_end.y;
535 let x3 = line2_start.x;
536 let y3 = line2_start.y;
537 let x4 = line2_end.x;
538 let y4 = line2_end.y;
539
540 let denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
541 if denominator.abs() < EPSILON_PARALLEL {
542 return None;
544 }
545
546 let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denominator;
547 let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denominator;
548
549 if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
551 let x = x1 + t * (x2 - x1);
552 let y = y1 + t * (y2 - y1);
553 return Some(Coords2d { x, y });
554 }
555
556 None
557}
558
559pub fn project_point_onto_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
564 let dx = segment_end.x - segment_start.x;
565 let dy = segment_end.y - segment_start.y;
566 let segment_length_sq = dx * dx + dy * dy;
567
568 if segment_length_sq < EPSILON_PARALLEL {
569 return 0.0;
571 }
572
573 let point_dx = point.x - segment_start.x;
574 let point_dy = point.y - segment_start.y;
575
576 (point_dx * dx + point_dy * dy) / segment_length_sq
577}
578
579pub fn perpendicular_distance_to_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
583 let dx = segment_end.x - segment_start.x;
584 let dy = segment_end.y - segment_start.y;
585 let segment_length_sq = dx * dx + dy * dy;
586
587 if segment_length_sq < EPSILON_PARALLEL {
588 let dist_dx = point.x - segment_start.x;
590 let dist_dy = point.y - segment_start.y;
591 return (dist_dx * dist_dx + dist_dy * dist_dy).sqrt();
592 }
593
594 let point_dx = point.x - segment_start.x;
596 let point_dy = point.y - segment_start.y;
597
598 let t = (point_dx * dx + point_dy * dy) / segment_length_sq;
600
601 let clamped_t = t.clamp(0.0, 1.0);
603 let closest_point = Coords2d {
604 x: segment_start.x + clamped_t * dx,
605 y: segment_start.y + clamped_t * dy,
606 };
607
608 let dist_dx = point.x - closest_point.x;
610 let dist_dy = point.y - closest_point.y;
611 (dist_dx * dist_dx + dist_dy * dist_dy).sqrt()
612}
613
614pub fn is_point_on_arc(point: Coords2d, center: Coords2d, start: Coords2d, end: Coords2d, epsilon: f64) -> bool {
618 let radius = ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
620
621 let dist_from_center =
623 ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
624 if (dist_from_center - radius).abs() > epsilon {
625 return false;
626 }
627
628 let start_angle = libm::atan2(start.y - center.y, start.x - center.x);
630 let end_angle = libm::atan2(end.y - center.y, end.x - center.x);
631 let point_angle = libm::atan2(point.y - center.y, point.x - center.x);
632
633 let normalize_angle = |angle: f64| -> f64 {
635 if !angle.is_finite() {
636 return angle;
637 }
638 let mut normalized = angle;
639 while normalized < 0.0 {
640 normalized += TAU;
641 }
642 while normalized >= TAU {
643 normalized -= TAU;
644 }
645 normalized
646 };
647
648 let normalized_start = normalize_angle(start_angle);
649 let normalized_end = normalize_angle(end_angle);
650 let normalized_point = normalize_angle(point_angle);
651
652 if normalized_start < normalized_end {
656 normalized_point >= normalized_start && normalized_point <= normalized_end
658 } else {
659 normalized_point >= normalized_start || normalized_point <= normalized_end
661 }
662}
663
664pub fn line_arc_intersection(
668 line_start: Coords2d,
669 line_end: Coords2d,
670 arc_center: Coords2d,
671 arc_start: Coords2d,
672 arc_end: Coords2d,
673 epsilon: f64,
674) -> Option<Coords2d> {
675 let radius = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
677 + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
678 .sqrt();
679
680 let translated_line_start = Coords2d {
682 x: line_start.x - arc_center.x,
683 y: line_start.y - arc_center.y,
684 };
685 let translated_line_end = Coords2d {
686 x: line_end.x - arc_center.x,
687 y: line_end.y - arc_center.y,
688 };
689
690 let dx = translated_line_end.x - translated_line_start.x;
692 let dy = translated_line_end.y - translated_line_start.y;
693
694 let a = dx * dx + dy * dy;
701 let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
702 let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
703 - radius * radius;
704
705 let discriminant = b * b - 4.0 * a * c;
706
707 if discriminant < 0.0 {
708 return None;
710 }
711
712 if a.abs() < EPSILON_PARALLEL {
713 let dist_from_center = (translated_line_start.x * translated_line_start.x
715 + translated_line_start.y * translated_line_start.y)
716 .sqrt();
717 if (dist_from_center - radius).abs() <= epsilon {
718 let point = line_start;
720 if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
721 return Some(point);
722 }
723 }
724 return None;
725 }
726
727 let sqrt_discriminant = discriminant.sqrt();
728 let t1 = (-b - sqrt_discriminant) / (2.0 * a);
729 let t2 = (-b + sqrt_discriminant) / (2.0 * a);
730
731 let mut candidates: Vec<(f64, Coords2d)> = Vec::new();
733 if (0.0..=1.0).contains(&t1) {
734 let point = Coords2d {
735 x: line_start.x + t1 * (line_end.x - line_start.x),
736 y: line_start.y + t1 * (line_end.y - line_start.y),
737 };
738 candidates.push((t1, point));
739 }
740 if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
741 let point = Coords2d {
742 x: line_start.x + t2 * (line_end.x - line_start.x),
743 y: line_start.y + t2 * (line_end.y - line_start.y),
744 };
745 candidates.push((t2, point));
746 }
747
748 for (_t, point) in candidates {
750 if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
751 return Some(point);
752 }
753 }
754
755 None
756}
757
758pub fn line_circle_intersections(
763 line_start: Coords2d,
764 line_end: Coords2d,
765 circle_center: Coords2d,
766 radius: f64,
767 epsilon: f64,
768) -> Vec<(f64, Coords2d)> {
769 let translated_line_start = Coords2d {
771 x: line_start.x - circle_center.x,
772 y: line_start.y - circle_center.y,
773 };
774 let translated_line_end = Coords2d {
775 x: line_end.x - circle_center.x,
776 y: line_end.y - circle_center.y,
777 };
778
779 let dx = translated_line_end.x - translated_line_start.x;
780 let dy = translated_line_end.y - translated_line_start.y;
781 let a = dx * dx + dy * dy;
782 let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
783 let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
784 - radius * radius;
785
786 if a.abs() < EPSILON_PARALLEL {
787 return Vec::new();
788 }
789
790 let discriminant = b * b - 4.0 * a * c;
791 if discriminant < 0.0 {
792 return Vec::new();
793 }
794
795 let sqrt_discriminant = discriminant.sqrt();
796 let mut intersections = Vec::new();
797
798 let t1 = (-b - sqrt_discriminant) / (2.0 * a);
799 if (0.0..=1.0).contains(&t1) {
800 intersections.push((
801 t1,
802 Coords2d {
803 x: line_start.x + t1 * (line_end.x - line_start.x),
804 y: line_start.y + t1 * (line_end.y - line_start.y),
805 },
806 ));
807 }
808
809 let t2 = (-b + sqrt_discriminant) / (2.0 * a);
810 if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
811 intersections.push((
812 t2,
813 Coords2d {
814 x: line_start.x + t2 * (line_end.x - line_start.x),
815 y: line_start.y + t2 * (line_end.y - line_start.y),
816 },
817 ));
818 }
819
820 intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
821 intersections
822}
823
824pub fn project_point_onto_circle(point: Coords2d, center: Coords2d, start: Coords2d) -> f64 {
830 let normalize_angle = |angle: f64| -> f64 {
831 if !angle.is_finite() {
832 return angle;
833 }
834 let mut normalized = angle;
835 while normalized < 0.0 {
836 normalized += TAU;
837 }
838 while normalized >= TAU {
839 normalized -= TAU;
840 }
841 normalized
842 };
843
844 let start_angle = normalize_angle(libm::atan2(start.y - center.y, start.x - center.x));
845 let point_angle = normalize_angle(libm::atan2(point.y - center.y, point.x - center.x));
846 let delta_ccw = (point_angle - start_angle).rem_euclid(TAU);
847 delta_ccw / TAU
848}
849
850fn is_point_on_circle(point: Coords2d, center: Coords2d, radius: f64, epsilon: f64) -> bool {
851 let dist = ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
852 (dist - radius).abs() <= epsilon
853}
854
855pub fn project_point_onto_arc(point: Coords2d, arc_center: Coords2d, arc_start: Coords2d, arc_end: Coords2d) -> f64 {
858 let start_angle = libm::atan2(arc_start.y - arc_center.y, arc_start.x - arc_center.x);
860 let end_angle = libm::atan2(arc_end.y - arc_center.y, arc_end.x - arc_center.x);
861 let point_angle = libm::atan2(point.y - arc_center.y, point.x - arc_center.x);
862
863 let normalize_angle = |angle: f64| -> f64 {
865 if !angle.is_finite() {
866 return angle;
867 }
868 let mut normalized = angle;
869 while normalized < 0.0 {
870 normalized += TAU;
871 }
872 while normalized >= TAU {
873 normalized -= TAU;
874 }
875 normalized
876 };
877
878 let normalized_start = normalize_angle(start_angle);
879 let normalized_end = normalize_angle(end_angle);
880 let normalized_point = normalize_angle(point_angle);
881
882 let arc_length = if normalized_start < normalized_end {
884 normalized_end - normalized_start
885 } else {
886 TAU - normalized_start + normalized_end
888 };
889
890 if arc_length < EPSILON_PARALLEL {
891 return 0.0;
893 }
894
895 let point_arc_length = if normalized_start < normalized_end {
897 if normalized_point >= normalized_start && normalized_point <= normalized_end {
898 normalized_point - normalized_start
899 } else {
900 let dist_to_start = (normalized_point - normalized_start)
902 .abs()
903 .min(TAU - (normalized_point - normalized_start).abs());
904 let dist_to_end = (normalized_point - normalized_end)
905 .abs()
906 .min(TAU - (normalized_point - normalized_end).abs());
907 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
908 }
909 } else {
910 if normalized_point >= normalized_start || normalized_point <= normalized_end {
912 if normalized_point >= normalized_start {
913 normalized_point - normalized_start
914 } else {
915 TAU - normalized_start + normalized_point
916 }
917 } else {
918 let dist_to_start = (normalized_point - normalized_start)
920 .abs()
921 .min(TAU - (normalized_point - normalized_start).abs());
922 let dist_to_end = (normalized_point - normalized_end)
923 .abs()
924 .min(TAU - (normalized_point - normalized_end).abs());
925 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
926 }
927 };
928
929 point_arc_length / arc_length
931}
932
933pub fn arc_arc_intersections(
937 arc1_center: Coords2d,
938 arc1_start: Coords2d,
939 arc1_end: Coords2d,
940 arc2_center: Coords2d,
941 arc2_start: Coords2d,
942 arc2_end: Coords2d,
943 epsilon: f64,
944) -> Vec<Coords2d> {
945 let r1 = ((arc1_start.x - arc1_center.x) * (arc1_start.x - arc1_center.x)
947 + (arc1_start.y - arc1_center.y) * (arc1_start.y - arc1_center.y))
948 .sqrt();
949 let r2 = ((arc2_start.x - arc2_center.x) * (arc2_start.x - arc2_center.x)
950 + (arc2_start.y - arc2_center.y) * (arc2_start.y - arc2_center.y))
951 .sqrt();
952
953 let dx = arc2_center.x - arc1_center.x;
955 let dy = arc2_center.y - arc1_center.y;
956 let d = (dx * dx + dy * dy).sqrt();
957
958 if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon {
960 return Vec::new();
962 }
963
964 if d < EPSILON_PARALLEL {
966 return Vec::new();
968 }
969
970 let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
973 let h_sq = r1 * r1 - a * a;
974
975 if h_sq < 0.0 {
977 return Vec::new();
978 }
979
980 let h = h_sq.sqrt();
981
982 if h.is_nan() {
984 return Vec::new();
985 }
986
987 let ux = dx / d;
989 let uy = dy / d;
990
991 let px = -uy;
993 let py = ux;
994
995 let mid_point = Coords2d {
997 x: arc1_center.x + a * ux,
998 y: arc1_center.y + a * uy,
999 };
1000
1001 let intersection1 = Coords2d {
1003 x: mid_point.x + h * px,
1004 y: mid_point.y + h * py,
1005 };
1006 let intersection2 = Coords2d {
1007 x: mid_point.x - h * px,
1008 y: mid_point.y - h * py,
1009 };
1010
1011 let mut candidates: Vec<Coords2d> = Vec::new();
1013
1014 if is_point_on_arc(intersection1, arc1_center, arc1_start, arc1_end, epsilon)
1015 && is_point_on_arc(intersection1, arc2_center, arc2_start, arc2_end, epsilon)
1016 {
1017 candidates.push(intersection1);
1018 }
1019
1020 if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1021 if is_point_on_arc(intersection2, arc1_center, arc1_start, arc1_end, epsilon)
1023 && is_point_on_arc(intersection2, arc2_center, arc2_start, arc2_end, epsilon)
1024 {
1025 candidates.push(intersection2);
1026 }
1027 }
1028
1029 candidates
1030}
1031
1032pub fn arc_arc_intersection(
1037 arc1_center: Coords2d,
1038 arc1_start: Coords2d,
1039 arc1_end: Coords2d,
1040 arc2_center: Coords2d,
1041 arc2_start: Coords2d,
1042 arc2_end: Coords2d,
1043 epsilon: f64,
1044) -> Option<Coords2d> {
1045 arc_arc_intersections(
1046 arc1_center,
1047 arc1_start,
1048 arc1_end,
1049 arc2_center,
1050 arc2_start,
1051 arc2_end,
1052 epsilon,
1053 )
1054 .first()
1055 .copied()
1056}
1057
1058pub fn circle_arc_intersections(
1062 circle_center: Coords2d,
1063 circle_radius: f64,
1064 arc_center: Coords2d,
1065 arc_start: Coords2d,
1066 arc_end: Coords2d,
1067 epsilon: f64,
1068) -> Vec<Coords2d> {
1069 let r1 = circle_radius;
1070 let r2 = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
1071 + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
1072 .sqrt();
1073
1074 let dx = arc_center.x - circle_center.x;
1075 let dy = arc_center.y - circle_center.y;
1076 let d = (dx * dx + dy * dy).sqrt();
1077
1078 if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon || d < EPSILON_PARALLEL {
1079 return Vec::new();
1080 }
1081
1082 let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1083 let h_sq = r1 * r1 - a * a;
1084 if h_sq < 0.0 {
1085 return Vec::new();
1086 }
1087 let h = h_sq.sqrt();
1088 if h.is_nan() {
1089 return Vec::new();
1090 }
1091
1092 let ux = dx / d;
1093 let uy = dy / d;
1094 let px = -uy;
1095 let py = ux;
1096 let mid_point = Coords2d {
1097 x: circle_center.x + a * ux,
1098 y: circle_center.y + a * uy,
1099 };
1100
1101 let intersection1 = Coords2d {
1102 x: mid_point.x + h * px,
1103 y: mid_point.y + h * py,
1104 };
1105 let intersection2 = Coords2d {
1106 x: mid_point.x - h * px,
1107 y: mid_point.y - h * py,
1108 };
1109
1110 let mut intersections = Vec::new();
1111 if is_point_on_arc(intersection1, arc_center, arc_start, arc_end, epsilon) {
1112 intersections.push(intersection1);
1113 }
1114 if ((intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon)
1115 && is_point_on_arc(intersection2, arc_center, arc_start, arc_end, epsilon)
1116 {
1117 intersections.push(intersection2);
1118 }
1119 intersections
1120}
1121
1122pub fn circle_circle_intersections(
1126 circle1_center: Coords2d,
1127 circle1_radius: f64,
1128 circle2_center: Coords2d,
1129 circle2_radius: f64,
1130 epsilon: f64,
1131) -> Vec<Coords2d> {
1132 let dx = circle2_center.x - circle1_center.x;
1133 let dy = circle2_center.y - circle1_center.y;
1134 let d = (dx * dx + dy * dy).sqrt();
1135
1136 if d > circle1_radius + circle2_radius + epsilon
1137 || d < (circle1_radius - circle2_radius).abs() - epsilon
1138 || d < EPSILON_PARALLEL
1139 {
1140 return Vec::new();
1141 }
1142
1143 let a = (circle1_radius * circle1_radius - circle2_radius * circle2_radius + d * d) / (2.0 * d);
1144 let h_sq = circle1_radius * circle1_radius - a * a;
1145 if h_sq < 0.0 {
1146 return Vec::new();
1147 }
1148
1149 let h = if h_sq <= epsilon { 0.0 } else { h_sq.sqrt() };
1150 if h.is_nan() {
1151 return Vec::new();
1152 }
1153
1154 let ux = dx / d;
1155 let uy = dy / d;
1156 let px = -uy;
1157 let py = ux;
1158
1159 let mid_point = Coords2d {
1160 x: circle1_center.x + a * ux,
1161 y: circle1_center.y + a * uy,
1162 };
1163
1164 let intersection1 = Coords2d {
1165 x: mid_point.x + h * px,
1166 y: mid_point.y + h * py,
1167 };
1168 let intersection2 = Coords2d {
1169 x: mid_point.x - h * px,
1170 y: mid_point.y - h * py,
1171 };
1172
1173 let mut intersections = vec![intersection1];
1174 if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1175 intersections.push(intersection2);
1176 }
1177 intersections
1178}
1179
1180fn get_point_coords_from_native(objects: &[Object], point_id: ObjectId, default_unit: UnitLength) -> Option<Coords2d> {
1183 let point_obj = objects.get(point_id.0)?;
1184
1185 let ObjectKind::Segment { segment } = &point_obj.kind else {
1187 return None;
1188 };
1189
1190 let Segment::Point(point) = segment else {
1191 return None;
1192 };
1193
1194 Some(Coords2d {
1196 x: number_to_unit(&point.position.x, default_unit),
1197 y: number_to_unit(&point.position.y, default_unit),
1198 })
1199}
1200
1201pub fn get_position_coords_for_line(
1204 segment_obj: &Object,
1205 which: LineEndpoint,
1206 objects: &[Object],
1207 default_unit: UnitLength,
1208) -> Option<Coords2d> {
1209 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1210 return None;
1211 };
1212
1213 let Segment::Line(line) = segment else {
1214 return None;
1215 };
1216
1217 let point_id = match which {
1219 LineEndpoint::Start => line.start,
1220 LineEndpoint::End => line.end,
1221 };
1222
1223 get_point_coords_from_native(objects, point_id, default_unit)
1224}
1225
1226fn is_point_coincident_with_segment_native(point_id: ObjectId, segment_id: ObjectId, objects: &[Object]) -> bool {
1228 for obj in objects {
1230 let ObjectKind::Constraint { constraint } = &obj.kind else {
1231 continue;
1232 };
1233
1234 let Constraint::Coincident(coincident) = constraint else {
1235 continue;
1236 };
1237
1238 let has_point = coincident.contains_segment(point_id);
1240 let has_segment = coincident.contains_segment(segment_id);
1241
1242 if has_point && has_segment {
1243 return true;
1244 }
1245 }
1246 false
1247}
1248
1249pub fn get_position_coords_from_arc(
1251 segment_obj: &Object,
1252 which: ArcPoint,
1253 objects: &[Object],
1254 default_unit: UnitLength,
1255) -> Option<Coords2d> {
1256 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1257 return None;
1258 };
1259
1260 let Segment::Arc(arc) = segment else {
1261 return None;
1262 };
1263
1264 let point_id = match which {
1266 ArcPoint::Start => arc.start,
1267 ArcPoint::End => arc.end,
1268 ArcPoint::Center => arc.center,
1269 };
1270
1271 get_point_coords_from_native(objects, point_id, default_unit)
1272}
1273
1274pub fn get_position_coords_from_circle(
1276 segment_obj: &Object,
1277 which: CirclePoint,
1278 objects: &[Object],
1279 default_unit: UnitLength,
1280) -> Option<Coords2d> {
1281 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1282 return None;
1283 };
1284
1285 let Segment::Circle(circle) = segment else {
1286 return None;
1287 };
1288
1289 let point_id = match which {
1290 CirclePoint::Start => circle.start,
1291 CirclePoint::Center => circle.center,
1292 };
1293
1294 get_point_coords_from_native(objects, point_id, default_unit)
1295}
1296
1297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1299enum CurveKind {
1300 Line,
1301 Circular,
1302}
1303
1304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1306enum CurveDomain {
1307 Open,
1308 Closed,
1309}
1310
1311#[derive(Debug, Clone, Copy)]
1313struct CurveHandle {
1314 segment_id: ObjectId,
1315 kind: CurveKind,
1316 domain: CurveDomain,
1317 start: Coords2d,
1318 end: Coords2d,
1319 center: Option<Coords2d>,
1320 radius: Option<f64>,
1321}
1322
1323impl CurveHandle {
1324 fn project_for_trim(self, point: Coords2d) -> Result<f64, String> {
1325 match (self.kind, self.domain) {
1326 (CurveKind::Line, CurveDomain::Open) => Ok(project_point_onto_segment(point, self.start, self.end)),
1327 (CurveKind::Circular, CurveDomain::Open) => {
1328 let center = self
1329 .center
1330 .ok_or_else(|| format!("Curve {} missing center for arc projection", self.segment_id.0))?;
1331 Ok(project_point_onto_arc(point, center, self.start, self.end))
1332 }
1333 (CurveKind::Circular, CurveDomain::Closed) => {
1334 let center = self
1335 .center
1336 .ok_or_else(|| format!("Curve {} missing center for circle projection", self.segment_id.0))?;
1337 Ok(project_point_onto_circle(point, center, self.start))
1338 }
1339 (CurveKind::Line, CurveDomain::Closed) => Err(format!(
1340 "Invalid curve state: line {} cannot be closed",
1341 self.segment_id.0
1342 )),
1343 }
1344 }
1345}
1346
1347fn load_curve_handle(
1349 segment_obj: &Object,
1350 objects: &[Object],
1351 default_unit: UnitLength,
1352) -> Result<CurveHandle, String> {
1353 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1354 return Err("Object is not a segment".to_owned());
1355 };
1356
1357 match segment {
1358 Segment::Line(_) => {
1359 let start = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit)
1360 .ok_or_else(|| format!("Could not get line start for segment {}", segment_obj.id.0))?;
1361 let end = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit)
1362 .ok_or_else(|| format!("Could not get line end for segment {}", segment_obj.id.0))?;
1363 Ok(CurveHandle {
1364 segment_id: segment_obj.id,
1365 kind: CurveKind::Line,
1366 domain: CurveDomain::Open,
1367 start,
1368 end,
1369 center: None,
1370 radius: None,
1371 })
1372 }
1373 Segment::Arc(_) => {
1374 let start = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit)
1375 .ok_or_else(|| format!("Could not get arc start for segment {}", segment_obj.id.0))?;
1376 let end = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit)
1377 .ok_or_else(|| format!("Could not get arc end for segment {}", segment_obj.id.0))?;
1378 let center = get_position_coords_from_arc(segment_obj, ArcPoint::Center, objects, default_unit)
1379 .ok_or_else(|| format!("Could not get arc center for segment {}", segment_obj.id.0))?;
1380 let radius =
1381 ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1382 Ok(CurveHandle {
1383 segment_id: segment_obj.id,
1384 kind: CurveKind::Circular,
1385 domain: CurveDomain::Open,
1386 start,
1387 end,
1388 center: Some(center),
1389 radius: Some(radius),
1390 })
1391 }
1392 Segment::Circle(_) => {
1393 let start = get_position_coords_from_circle(segment_obj, CirclePoint::Start, objects, default_unit)
1394 .ok_or_else(|| format!("Could not get circle start for segment {}", segment_obj.id.0))?;
1395 let center = get_position_coords_from_circle(segment_obj, CirclePoint::Center, objects, default_unit)
1396 .ok_or_else(|| format!("Could not get circle center for segment {}", segment_obj.id.0))?;
1397 let radius =
1398 ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1399 Ok(CurveHandle {
1400 segment_id: segment_obj.id,
1401 kind: CurveKind::Circular,
1402 domain: CurveDomain::Closed,
1403 start,
1404 end: start,
1406 center: Some(center),
1407 radius: Some(radius),
1408 })
1409 }
1410 Segment::Point(_) => Err(format!(
1411 "Point segment {} cannot be used as trim curve",
1412 segment_obj.id.0
1413 )),
1414 }
1415}
1416
1417fn project_point_onto_curve(curve: CurveHandle, point: Coords2d) -> Result<f64, String> {
1418 curve.project_for_trim(point)
1419}
1420
1421fn curve_contains_point(curve: CurveHandle, point: Coords2d, epsilon: f64) -> bool {
1422 match (curve.kind, curve.domain) {
1423 (CurveKind::Line, CurveDomain::Open) => {
1424 let t = project_point_onto_segment(point, curve.start, curve.end);
1425 (0.0..=1.0).contains(&t) && perpendicular_distance_to_segment(point, curve.start, curve.end) <= epsilon
1426 }
1427 (CurveKind::Circular, CurveDomain::Open) => curve
1428 .center
1429 .is_some_and(|center| is_point_on_arc(point, center, curve.start, curve.end, epsilon)),
1430 (CurveKind::Circular, CurveDomain::Closed) => curve.center.is_some_and(|center| {
1431 let radius = curve
1432 .radius
1433 .unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
1434 is_point_on_circle(point, center, radius, epsilon)
1435 }),
1436 (CurveKind::Line, CurveDomain::Closed) => false,
1437 }
1438}
1439
1440fn curve_line_segment_intersections(
1441 curve: CurveHandle,
1442 line_start: Coords2d,
1443 line_end: Coords2d,
1444 epsilon: f64,
1445) -> Vec<(f64, Coords2d)> {
1446 match (curve.kind, curve.domain) {
1447 (CurveKind::Line, CurveDomain::Open) => {
1448 line_segment_intersection(line_start, line_end, curve.start, curve.end, epsilon)
1449 .map(|intersection| {
1450 (
1451 project_point_onto_segment(intersection, line_start, line_end),
1452 intersection,
1453 )
1454 })
1455 .into_iter()
1456 .collect()
1457 }
1458 (CurveKind::Circular, CurveDomain::Open) => curve
1459 .center
1460 .and_then(|center| line_arc_intersection(line_start, line_end, center, curve.start, curve.end, epsilon))
1461 .map(|intersection| {
1462 (
1463 project_point_onto_segment(intersection, line_start, line_end),
1464 intersection,
1465 )
1466 })
1467 .into_iter()
1468 .collect(),
1469 (CurveKind::Circular, CurveDomain::Closed) => {
1470 let Some(center) = curve.center else {
1471 return Vec::new();
1472 };
1473 let radius = curve
1474 .radius
1475 .unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
1476 line_circle_intersections(line_start, line_end, center, radius, epsilon)
1477 }
1478 (CurveKind::Line, CurveDomain::Closed) => Vec::new(),
1479 }
1480}
1481
1482fn curve_polyline_intersections(curve: CurveHandle, polyline: &[Coords2d], epsilon: f64) -> Vec<(Coords2d, usize)> {
1483 let mut intersections = Vec::new();
1484
1485 for i in 0..polyline.len().saturating_sub(1) {
1486 let p1 = polyline[i];
1487 let p2 = polyline[i + 1];
1488 for (_, intersection) in curve_line_segment_intersections(curve, p1, p2, epsilon) {
1489 intersections.push((intersection, i));
1490 }
1491 }
1492
1493 intersections
1494}
1495
1496fn curve_curve_intersections(curve: CurveHandle, other: CurveHandle, epsilon: f64) -> Vec<Coords2d> {
1497 match (curve.kind, curve.domain, other.kind, other.domain) {
1498 (CurveKind::Line, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => {
1499 line_segment_intersection(curve.start, curve.end, other.start, other.end, epsilon)
1500 .into_iter()
1501 .collect()
1502 }
1503 (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => other
1504 .center
1505 .and_then(|other_center| {
1506 line_arc_intersection(curve.start, curve.end, other_center, other.start, other.end, epsilon)
1507 })
1508 .into_iter()
1509 .collect(),
1510 (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
1511 let Some(other_center) = other.center else {
1512 return Vec::new();
1513 };
1514 let other_radius = other.radius.unwrap_or_else(|| {
1515 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1516 });
1517 line_circle_intersections(curve.start, curve.end, other_center, other_radius, epsilon)
1518 .into_iter()
1519 .map(|(_, point)| point)
1520 .collect()
1521 }
1522 (CurveKind::Circular, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => curve
1523 .center
1524 .and_then(|curve_center| {
1525 line_arc_intersection(other.start, other.end, curve_center, curve.start, curve.end, epsilon)
1526 })
1527 .into_iter()
1528 .collect(),
1529 (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => {
1530 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1531 return Vec::new();
1532 };
1533 arc_arc_intersections(
1534 curve_center,
1535 curve.start,
1536 curve.end,
1537 other_center,
1538 other.start,
1539 other.end,
1540 epsilon,
1541 )
1542 }
1543 (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
1544 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1545 return Vec::new();
1546 };
1547 let other_radius = other.radius.unwrap_or_else(|| {
1548 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1549 });
1550 circle_arc_intersections(
1551 other_center,
1552 other_radius,
1553 curve_center,
1554 curve.start,
1555 curve.end,
1556 epsilon,
1557 )
1558 }
1559 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Line, CurveDomain::Open) => {
1560 let Some(curve_center) = curve.center else {
1561 return Vec::new();
1562 };
1563 let curve_radius = curve.radius.unwrap_or_else(|| {
1564 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1565 });
1566 line_circle_intersections(other.start, other.end, curve_center, curve_radius, epsilon)
1567 .into_iter()
1568 .map(|(_, point)| point)
1569 .collect()
1570 }
1571 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Open) => {
1572 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1573 return Vec::new();
1574 };
1575 let curve_radius = curve.radius.unwrap_or_else(|| {
1576 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1577 });
1578 circle_arc_intersections(
1579 curve_center,
1580 curve_radius,
1581 other_center,
1582 other.start,
1583 other.end,
1584 epsilon,
1585 )
1586 }
1587 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Closed) => {
1588 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1589 return Vec::new();
1590 };
1591 let curve_radius = curve.radius.unwrap_or_else(|| {
1592 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1593 });
1594 let other_radius = other.radius.unwrap_or_else(|| {
1595 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1596 });
1597 circle_circle_intersections(curve_center, curve_radius, other_center, other_radius, epsilon)
1598 }
1599 _ => Vec::new(),
1600 }
1601}
1602
1603fn segment_endpoint_points(
1604 segment_obj: &Object,
1605 objects: &[Object],
1606 default_unit: UnitLength,
1607) -> Vec<(ObjectId, Coords2d)> {
1608 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1609 return Vec::new();
1610 };
1611
1612 match segment {
1613 Segment::Line(line) => {
1614 let mut points = Vec::new();
1615 if let Some(start) = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit) {
1616 points.push((line.start, start));
1617 }
1618 if let Some(end) = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit) {
1619 points.push((line.end, end));
1620 }
1621 points
1622 }
1623 Segment::Arc(arc) => {
1624 let mut points = Vec::new();
1625 if let Some(start) = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit) {
1626 points.push((arc.start, start));
1627 }
1628 if let Some(end) = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit) {
1629 points.push((arc.end, end));
1630 }
1631 points
1632 }
1633 _ => Vec::new(),
1634 }
1635}
1636
1637pub fn get_next_trim_spawn(
1665 points: &[Coords2d],
1666 start_index: usize,
1667 objects: &[Object],
1668 default_unit: UnitLength,
1669) -> TrimItem {
1670 let scene_curves: Vec<CurveHandle> = objects
1671 .iter()
1672 .filter_map(|obj| load_curve_handle(obj, objects, default_unit).ok())
1673 .collect();
1674
1675 for i in start_index..points.len().saturating_sub(1) {
1677 let p1 = points[i];
1678 let p2 = points[i + 1];
1679
1680 for curve in &scene_curves {
1682 let intersections = curve_line_segment_intersections(*curve, p1, p2, EPSILON_POINT_ON_SEGMENT);
1683 if let Some((_, intersection)) = intersections.first() {
1684 return TrimItem::Spawn {
1685 trim_spawn_seg_id: curve.segment_id,
1686 trim_spawn_coords: *intersection,
1687 next_index: i,
1688 };
1689 }
1690 }
1691 }
1692
1693 TrimItem::None {
1695 next_index: points.len().saturating_sub(1),
1696 }
1697}
1698
1699pub fn get_trim_spawn_terminations(
1754 trim_spawn_seg_id: ObjectId,
1755 trim_spawn_coords: &[Coords2d],
1756 objects: &[Object],
1757 default_unit: UnitLength,
1758) -> Result<TrimTerminations, String> {
1759 let trim_spawn_seg = objects.iter().find(|obj| obj.id == trim_spawn_seg_id);
1761
1762 let trim_spawn_seg = match trim_spawn_seg {
1763 Some(seg) => seg,
1764 None => {
1765 return Err(format!("Trim spawn segment {} not found", trim_spawn_seg_id.0));
1766 }
1767 };
1768
1769 let trim_curve = load_curve_handle(trim_spawn_seg, objects, default_unit).map_err(|e| {
1770 format!(
1771 "Failed to load trim spawn segment {} as normalized curve: {}",
1772 trim_spawn_seg_id.0, e
1773 )
1774 })?;
1775
1776 let all_intersections = curve_polyline_intersections(trim_curve, trim_spawn_coords, EPSILON_POINT_ON_SEGMENT);
1781
1782 let intersection_point = if all_intersections.is_empty() {
1785 return Err("Could not find intersection point between polyline and trim spawn segment".to_string());
1786 } else {
1787 let mid_index = (trim_spawn_coords.len() - 1) / 2;
1789 let mid_point = trim_spawn_coords[mid_index];
1790
1791 let mut min_dist = f64::INFINITY;
1793 let mut closest_intersection = all_intersections[0].0;
1794
1795 for (intersection, _) in &all_intersections {
1796 let dist = ((intersection.x - mid_point.x) * (intersection.x - mid_point.x)
1797 + (intersection.y - mid_point.y) * (intersection.y - mid_point.y))
1798 .sqrt();
1799 if dist < min_dist {
1800 min_dist = dist;
1801 closest_intersection = *intersection;
1802 }
1803 }
1804
1805 closest_intersection
1806 };
1807
1808 let intersection_t = project_point_onto_curve(trim_curve, intersection_point)?;
1810
1811 let left_termination = find_termination_in_direction(
1813 trim_spawn_seg,
1814 trim_curve,
1815 intersection_t,
1816 TrimDirection::Left,
1817 objects,
1818 default_unit,
1819 )?;
1820
1821 let right_termination = find_termination_in_direction(
1822 trim_spawn_seg,
1823 trim_curve,
1824 intersection_t,
1825 TrimDirection::Right,
1826 objects,
1827 default_unit,
1828 )?;
1829
1830 Ok(TrimTerminations {
1831 left_side: left_termination,
1832 right_side: right_termination,
1833 })
1834}
1835
1836fn find_termination_in_direction(
1889 trim_spawn_seg: &Object,
1890 trim_curve: CurveHandle,
1891 intersection_t: f64,
1892 direction: TrimDirection,
1893 objects: &[Object],
1894 default_unit: UnitLength,
1895) -> Result<TrimTermination, String> {
1896 let ObjectKind::Segment { segment } = &trim_spawn_seg.kind else {
1898 return Err("Trim spawn segment is not a segment".to_string());
1899 };
1900
1901 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1903 enum CandidateType {
1904 Intersection,
1905 Coincident,
1906 Endpoint,
1907 }
1908
1909 #[derive(Debug, Clone)]
1910 struct Candidate {
1911 t: f64,
1912 point: Coords2d,
1913 candidate_type: CandidateType,
1914 segment_id: Option<ObjectId>,
1915 point_id: Option<ObjectId>,
1916 }
1917
1918 let mut candidates: Vec<Candidate> = Vec::new();
1919
1920 match segment {
1922 Segment::Line(line) => {
1923 candidates.push(Candidate {
1924 t: 0.0,
1925 point: trim_curve.start,
1926 candidate_type: CandidateType::Endpoint,
1927 segment_id: None,
1928 point_id: Some(line.start),
1929 });
1930 candidates.push(Candidate {
1931 t: 1.0,
1932 point: trim_curve.end,
1933 candidate_type: CandidateType::Endpoint,
1934 segment_id: None,
1935 point_id: Some(line.end),
1936 });
1937 }
1938 Segment::Arc(arc) => {
1939 candidates.push(Candidate {
1941 t: 0.0,
1942 point: trim_curve.start,
1943 candidate_type: CandidateType::Endpoint,
1944 segment_id: None,
1945 point_id: Some(arc.start),
1946 });
1947 candidates.push(Candidate {
1948 t: 1.0,
1949 point: trim_curve.end,
1950 candidate_type: CandidateType::Endpoint,
1951 segment_id: None,
1952 point_id: Some(arc.end),
1953 });
1954 }
1955 Segment::Circle(_) => {
1956 }
1958 _ => {}
1959 }
1960
1961 let trim_spawn_seg_id = trim_spawn_seg.id;
1963
1964 for other_seg in objects.iter() {
1966 let other_id = other_seg.id;
1967 if other_id == trim_spawn_seg_id {
1968 continue;
1969 }
1970
1971 if let Ok(other_curve) = load_curve_handle(other_seg, objects, default_unit) {
1972 for intersection in curve_curve_intersections(trim_curve, other_curve, EPSILON_POINT_ON_SEGMENT) {
1973 let Ok(t) = project_point_onto_curve(trim_curve, intersection) else {
1974 continue;
1975 };
1976 candidates.push(Candidate {
1977 t,
1978 point: intersection,
1979 candidate_type: CandidateType::Intersection,
1980 segment_id: Some(other_id),
1981 point_id: None,
1982 });
1983 }
1984 }
1985
1986 for (other_point_id, other_point) in segment_endpoint_points(other_seg, objects, default_unit) {
1987 if !is_point_coincident_with_segment_native(other_point_id, trim_spawn_seg_id, objects) {
1988 continue;
1989 }
1990 if !curve_contains_point(trim_curve, other_point, EPSILON_POINT_ON_SEGMENT) {
1991 continue;
1992 }
1993 let Ok(t) = project_point_onto_curve(trim_curve, other_point) else {
1994 continue;
1995 };
1996 candidates.push(Candidate {
1997 t,
1998 point: other_point,
1999 candidate_type: CandidateType::Coincident,
2000 segment_id: Some(other_id),
2001 point_id: Some(other_point_id),
2002 });
2003 }
2004 }
2005
2006 let is_circle_segment = trim_curve.domain == CurveDomain::Closed;
2007
2008 let intersection_epsilon = EPSILON_POINT_ON_SEGMENT * 10.0; let direction_distance = |candidate_t: f64| -> f64 {
2012 if is_circle_segment {
2013 match direction {
2014 TrimDirection::Left => (intersection_t - candidate_t).rem_euclid(1.0),
2015 TrimDirection::Right => (candidate_t - intersection_t).rem_euclid(1.0),
2016 }
2017 } else {
2018 (candidate_t - intersection_t).abs()
2019 }
2020 };
2021 let filtered_candidates: Vec<Candidate> = candidates
2022 .into_iter()
2023 .filter(|candidate| {
2024 let dist_from_intersection = if is_circle_segment {
2025 let ccw = (candidate.t - intersection_t).rem_euclid(1.0);
2026 let cw = (intersection_t - candidate.t).rem_euclid(1.0);
2027 ccw.min(cw)
2028 } else {
2029 (candidate.t - intersection_t).abs()
2030 };
2031 if dist_from_intersection < intersection_epsilon {
2032 return false; }
2034
2035 if is_circle_segment {
2036 direction_distance(candidate.t) > intersection_epsilon
2037 } else {
2038 match direction {
2039 TrimDirection::Left => candidate.t < intersection_t,
2040 TrimDirection::Right => candidate.t > intersection_t,
2041 }
2042 }
2043 })
2044 .collect();
2045
2046 let mut sorted_candidates = filtered_candidates;
2049 sorted_candidates.sort_by(|a, b| {
2050 let dist_a = direction_distance(a.t);
2051 let dist_b = direction_distance(b.t);
2052 let dist_diff = dist_a - dist_b;
2053 if dist_diff.abs() > EPSILON_POINT_ON_SEGMENT {
2054 dist_diff.partial_cmp(&0.0).unwrap_or(std::cmp::Ordering::Equal)
2055 } else {
2056 let type_priority = |candidate_type: CandidateType| -> i32 {
2058 match candidate_type {
2059 CandidateType::Coincident => 0,
2060 CandidateType::Intersection => 1,
2061 CandidateType::Endpoint => 2,
2062 }
2063 };
2064 type_priority(a.candidate_type).cmp(&type_priority(b.candidate_type))
2065 }
2066 });
2067
2068 let closest_candidate = match sorted_candidates.first() {
2070 Some(c) => c,
2071 None => {
2072 if is_circle_segment {
2073 return Err("No trim termination candidate found for circle".to_string());
2074 }
2075 let endpoint = match direction {
2077 TrimDirection::Left => trim_curve.start,
2078 TrimDirection::Right => trim_curve.end,
2079 };
2080 return Ok(TrimTermination::SegEndPoint {
2081 trim_termination_coords: endpoint,
2082 });
2083 }
2084 };
2085
2086 if !is_circle_segment
2090 && closest_candidate.candidate_type == CandidateType::Intersection
2091 && let Some(seg_id) = closest_candidate.segment_id
2092 {
2093 let intersecting_seg = objects.iter().find(|obj| obj.id == seg_id);
2094
2095 if let Some(intersecting_seg) = intersecting_seg {
2096 let endpoint_epsilon = EPSILON_POINT_ON_SEGMENT * 1000.0; let is_other_seg_endpoint = segment_endpoint_points(intersecting_seg, objects, default_unit)
2099 .into_iter()
2100 .any(|(_, endpoint)| {
2101 let dist_to_endpoint = ((closest_candidate.point.x - endpoint.x).powi(2)
2102 + (closest_candidate.point.y - endpoint.y).powi(2))
2103 .sqrt();
2104 dist_to_endpoint < endpoint_epsilon
2105 });
2106
2107 if is_other_seg_endpoint {
2110 let endpoint = match direction {
2111 TrimDirection::Left => trim_curve.start,
2112 TrimDirection::Right => trim_curve.end,
2113 };
2114 return Ok(TrimTermination::SegEndPoint {
2115 trim_termination_coords: endpoint,
2116 });
2117 }
2118 }
2119
2120 let endpoint_t = match direction {
2122 TrimDirection::Left => 0.0,
2123 TrimDirection::Right => 1.0,
2124 };
2125 let endpoint = match direction {
2126 TrimDirection::Left => trim_curve.start,
2127 TrimDirection::Right => trim_curve.end,
2128 };
2129 let dist_to_endpoint_param = (closest_candidate.t - endpoint_t).abs();
2130 let dist_to_endpoint_coords = ((closest_candidate.point.x - endpoint.x)
2131 * (closest_candidate.point.x - endpoint.x)
2132 + (closest_candidate.point.y - endpoint.y) * (closest_candidate.point.y - endpoint.y))
2133 .sqrt();
2134
2135 let is_at_endpoint =
2136 dist_to_endpoint_param < EPSILON_POINT_ON_SEGMENT || dist_to_endpoint_coords < EPSILON_POINT_ON_SEGMENT;
2137
2138 if is_at_endpoint {
2139 return Ok(TrimTermination::SegEndPoint {
2141 trim_termination_coords: endpoint,
2142 });
2143 }
2144 }
2145
2146 let endpoint_t_for_return = match direction {
2148 TrimDirection::Left => 0.0,
2149 TrimDirection::Right => 1.0,
2150 };
2151 if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Intersection {
2152 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2153 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2154 let endpoint = match direction {
2157 TrimDirection::Left => trim_curve.start,
2158 TrimDirection::Right => trim_curve.end,
2159 };
2160 return Ok(TrimTermination::SegEndPoint {
2161 trim_termination_coords: endpoint,
2162 });
2163 }
2164 }
2165
2166 let endpoint = match direction {
2168 TrimDirection::Left => trim_curve.start,
2169 TrimDirection::Right => trim_curve.end,
2170 };
2171 if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Endpoint {
2172 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2173 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2174 return Ok(TrimTermination::SegEndPoint {
2176 trim_termination_coords: endpoint,
2177 });
2178 }
2179 }
2180
2181 if closest_candidate.candidate_type == CandidateType::Coincident {
2183 Ok(TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2185 trim_termination_coords: closest_candidate.point,
2186 intersecting_seg_id: closest_candidate
2187 .segment_id
2188 .ok_or_else(|| "Missing segment_id for coincident".to_string())?,
2189 other_segment_point_id: closest_candidate
2190 .point_id
2191 .ok_or_else(|| "Missing point_id for coincident".to_string())?,
2192 })
2193 } else if closest_candidate.candidate_type == CandidateType::Intersection {
2194 Ok(TrimTermination::Intersection {
2195 trim_termination_coords: closest_candidate.point,
2196 intersecting_seg_id: closest_candidate
2197 .segment_id
2198 .ok_or_else(|| "Missing segment_id for intersection".to_string())?,
2199 })
2200 } else {
2201 if is_circle_segment {
2202 return Err("Circle trim termination unexpectedly resolved to endpoint".to_string());
2203 }
2204 Ok(TrimTermination::SegEndPoint {
2206 trim_termination_coords: closest_candidate.point,
2207 })
2208 }
2209}
2210
2211#[cfg(test)]
2222#[allow(dead_code)]
2223pub(crate) async fn execute_trim_loop<F, Fut>(
2224 points: &[Coords2d],
2225 default_unit: UnitLength,
2226 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2227 mut execute_operations: F,
2228) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>
2229where
2230 F: FnMut(Vec<TrimOperation>, crate::frontend::api::SceneGraphDelta) -> Fut,
2231 Fut: std::future::Future<
2232 Output = Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>,
2233 >,
2234{
2235 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2237 let points = normalized_points.as_slice();
2238
2239 let mut start_index = 0;
2240 let max_iterations = 1000;
2241 let mut iteration_count = 0;
2242 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2243 crate::frontend::api::SourceDelta { text: String::new() },
2244 initial_scene_graph_delta.clone(),
2245 ));
2246 let mut invalidates_ids = false;
2247 let mut current_scene_graph_delta = initial_scene_graph_delta;
2248 let circle_delete_fallback_strategy =
2249 |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
2250 if !error.contains("No trim termination candidate found for circle") {
2251 return None;
2252 }
2253 let is_circle = scene_objects
2254 .iter()
2255 .find(|obj| obj.id == segment_id)
2256 .is_some_and(|obj| {
2257 matches!(
2258 obj.kind,
2259 ObjectKind::Segment {
2260 segment: Segment::Circle(_)
2261 }
2262 )
2263 });
2264 if is_circle {
2265 Some(vec![TrimOperation::SimpleTrim {
2266 segment_to_trim_id: segment_id,
2267 }])
2268 } else {
2269 None
2270 }
2271 };
2272
2273 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2274 iteration_count += 1;
2275
2276 let next_trim_spawn = get_next_trim_spawn(
2278 points,
2279 start_index,
2280 ¤t_scene_graph_delta.new_graph.objects,
2281 default_unit,
2282 );
2283
2284 match &next_trim_spawn {
2285 TrimItem::None { next_index } => {
2286 let old_start_index = start_index;
2287 start_index = *next_index;
2288
2289 if start_index <= old_start_index {
2291 start_index = old_start_index + 1;
2292 }
2293
2294 if start_index >= points.len().saturating_sub(1) {
2296 break;
2297 }
2298 continue;
2299 }
2300 TrimItem::Spawn {
2301 trim_spawn_seg_id,
2302 trim_spawn_coords,
2303 next_index,
2304 ..
2305 } => {
2306 let terminations = match get_trim_spawn_terminations(
2308 *trim_spawn_seg_id,
2309 points,
2310 ¤t_scene_graph_delta.new_graph.objects,
2311 default_unit,
2312 ) {
2313 Ok(terms) => terms,
2314 Err(e) => {
2315 crate::logln!("Error getting trim spawn terminations: {}", e);
2316 if let Some(strategy) = circle_delete_fallback_strategy(
2317 &e,
2318 *trim_spawn_seg_id,
2319 ¤t_scene_graph_delta.new_graph.objects,
2320 ) {
2321 match execute_operations(strategy, current_scene_graph_delta.clone()).await {
2322 Ok((source_delta, scene_graph_delta)) => {
2323 last_result = Some((source_delta, scene_graph_delta.clone()));
2324 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2325 current_scene_graph_delta = scene_graph_delta;
2326 }
2327 Err(exec_err) => {
2328 crate::logln!(
2329 "Error executing circle-delete fallback trim operation: {}",
2330 exec_err
2331 );
2332 }
2333 }
2334
2335 let old_start_index = start_index;
2336 start_index = *next_index;
2337 if start_index <= old_start_index {
2338 start_index = old_start_index + 1;
2339 }
2340 continue;
2341 }
2342
2343 let old_start_index = start_index;
2344 start_index = *next_index;
2345 if start_index <= old_start_index {
2346 start_index = old_start_index + 1;
2347 }
2348 continue;
2349 }
2350 };
2351
2352 let trim_spawn_segment = current_scene_graph_delta
2354 .new_graph
2355 .objects
2356 .iter()
2357 .find(|obj| obj.id == *trim_spawn_seg_id)
2358 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
2359
2360 let plan = match build_trim_plan(
2361 *trim_spawn_seg_id,
2362 *trim_spawn_coords,
2363 trim_spawn_segment,
2364 &terminations.left_side,
2365 &terminations.right_side,
2366 ¤t_scene_graph_delta.new_graph.objects,
2367 default_unit,
2368 ) {
2369 Ok(plan) => plan,
2370 Err(e) => {
2371 crate::logln!("Error determining trim strategy: {}", e);
2372 let old_start_index = start_index;
2373 start_index = *next_index;
2374 if start_index <= old_start_index {
2375 start_index = old_start_index + 1;
2376 }
2377 continue;
2378 }
2379 };
2380 let strategy = lower_trim_plan(&plan);
2381
2382 let geometry_was_modified = trim_plan_modifies_geometry(&plan);
2385
2386 match execute_operations(strategy, current_scene_graph_delta.clone()).await {
2388 Ok((source_delta, scene_graph_delta)) => {
2389 last_result = Some((source_delta, scene_graph_delta.clone()));
2390 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2391 current_scene_graph_delta = scene_graph_delta;
2392 }
2393 Err(e) => {
2394 crate::logln!("Error executing trim operations: {}", e);
2395 }
2397 }
2398
2399 let old_start_index = start_index;
2401 start_index = *next_index;
2402
2403 if start_index <= old_start_index && !geometry_was_modified {
2405 start_index = old_start_index + 1;
2406 }
2407 }
2408 }
2409 }
2410
2411 if iteration_count >= max_iterations {
2412 return Err(format!("Reached max iterations ({})", max_iterations));
2413 }
2414
2415 last_result.ok_or_else(|| "No trim operations were executed".to_string())
2417}
2418
2419#[cfg(all(feature = "artifact-graph", test))]
2421#[derive(Debug, Clone)]
2422pub struct TrimFlowResult {
2423 pub kcl_code: String,
2424 pub invalidates_ids: bool,
2425}
2426
2427#[cfg(all(not(target_arch = "wasm32"), feature = "artifact-graph", test))]
2443pub(crate) async fn execute_trim_flow(
2444 kcl_code: &str,
2445 trim_points: &[Coords2d],
2446 sketch_id: ObjectId,
2447) -> Result<TrimFlowResult, String> {
2448 use crate::ExecutorContext;
2449 use crate::Program;
2450 use crate::execution::MockConfig;
2451 use crate::frontend::FrontendState;
2452 use crate::frontend::api::Version;
2453
2454 let parse_result = Program::parse(kcl_code).map_err(|e| format!("Failed to parse KCL: {}", e))?;
2456 let (program_opt, errors) = parse_result;
2457 if !errors.is_empty() {
2458 return Err(format!("Failed to parse KCL: {:?}", errors));
2459 }
2460 let program = program_opt.ok_or_else(|| "No AST produced".to_string())?;
2461
2462 let mock_ctx = ExecutorContext::new_mock(None).await;
2463
2464 let result = async {
2466 let mut frontend = FrontendState::new();
2467
2468 frontend.program = program.clone();
2470
2471 let exec_outcome = mock_ctx
2472 .run_mock(&program, &MockConfig::default())
2473 .await
2474 .map_err(|e| format!("Failed to execute program: {}", e.error.message()))?;
2475
2476 let exec_outcome = frontend.update_state_after_exec(exec_outcome, false);
2477 #[allow(unused_mut)] let mut initial_scene_graph = frontend.scene_graph.clone();
2479
2480 #[cfg(feature = "artifact-graph")]
2483 if initial_scene_graph.objects.is_empty() && !exec_outcome.scene_objects.is_empty() {
2484 initial_scene_graph.objects = exec_outcome.scene_objects.clone();
2485 }
2486
2487 let actual_sketch_id = if let Some(sketch_mode) = initial_scene_graph.sketch_mode {
2490 sketch_mode
2491 } else {
2492 initial_scene_graph
2494 .objects
2495 .iter()
2496 .find(|obj| matches!(obj.kind, crate::frontend::api::ObjectKind::Sketch { .. }))
2497 .map(|obj| obj.id)
2498 .unwrap_or(sketch_id) };
2500
2501 let version = Version(0);
2502 let initial_scene_graph_delta = crate::frontend::api::SceneGraphDelta {
2503 new_graph: initial_scene_graph,
2504 new_objects: vec![],
2505 invalidates_ids: false,
2506 exec_outcome,
2507 };
2508
2509 let (source_delta, scene_graph_delta) = execute_trim_loop_with_context(
2514 trim_points,
2515 initial_scene_graph_delta,
2516 &mut frontend,
2517 &mock_ctx,
2518 version,
2519 actual_sketch_id,
2520 )
2521 .await?;
2522
2523 if source_delta.text.is_empty() {
2526 return Err("No trim operations were executed - source delta is empty".to_string());
2527 }
2528
2529 Ok(TrimFlowResult {
2530 kcl_code: source_delta.text,
2531 invalidates_ids: scene_graph_delta.invalidates_ids,
2532 })
2533 }
2534 .await;
2535
2536 mock_ctx.close().await;
2538
2539 result
2540}
2541
2542pub async fn execute_trim_loop_with_context(
2548 points: &[Coords2d],
2549 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2550 frontend: &mut crate::frontend::FrontendState,
2551 ctx: &crate::ExecutorContext,
2552 version: crate::frontend::api::Version,
2553 sketch_id: ObjectId,
2554) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
2555 let default_unit = frontend.default_length_unit();
2557 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2558
2559 let mut current_scene_graph_delta = initial_scene_graph_delta.clone();
2562 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2563 crate::frontend::api::SourceDelta { text: String::new() },
2564 initial_scene_graph_delta.clone(),
2565 ));
2566 let mut invalidates_ids = false;
2567 let mut start_index = 0;
2568 let max_iterations = 1000;
2569 let mut iteration_count = 0;
2570 let circle_delete_fallback_strategy =
2571 |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
2572 if !error.contains("No trim termination candidate found for circle") {
2573 return None;
2574 }
2575 let is_circle = scene_objects
2576 .iter()
2577 .find(|obj| obj.id == segment_id)
2578 .is_some_and(|obj| {
2579 matches!(
2580 obj.kind,
2581 ObjectKind::Segment {
2582 segment: Segment::Circle(_)
2583 }
2584 )
2585 });
2586 if is_circle {
2587 Some(vec![TrimOperation::SimpleTrim {
2588 segment_to_trim_id: segment_id,
2589 }])
2590 } else {
2591 None
2592 }
2593 };
2594
2595 let points = normalized_points.as_slice();
2596
2597 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2598 iteration_count += 1;
2599
2600 let next_trim_spawn = get_next_trim_spawn(
2602 points,
2603 start_index,
2604 ¤t_scene_graph_delta.new_graph.objects,
2605 default_unit,
2606 );
2607
2608 match &next_trim_spawn {
2609 TrimItem::None { next_index } => {
2610 let old_start_index = start_index;
2611 start_index = *next_index;
2612 if start_index <= old_start_index {
2613 start_index = old_start_index + 1;
2614 }
2615 if start_index >= points.len().saturating_sub(1) {
2616 break;
2617 }
2618 continue;
2619 }
2620 TrimItem::Spawn {
2621 trim_spawn_seg_id,
2622 trim_spawn_coords,
2623 next_index,
2624 ..
2625 } => {
2626 let terminations = match get_trim_spawn_terminations(
2628 *trim_spawn_seg_id,
2629 points,
2630 ¤t_scene_graph_delta.new_graph.objects,
2631 default_unit,
2632 ) {
2633 Ok(terms) => terms,
2634 Err(e) => {
2635 crate::logln!("Error getting trim spawn terminations: {}", e);
2636 if let Some(strategy) = circle_delete_fallback_strategy(
2637 &e,
2638 *trim_spawn_seg_id,
2639 ¤t_scene_graph_delta.new_graph.objects,
2640 ) {
2641 match execute_trim_operations_simple(
2642 strategy.clone(),
2643 ¤t_scene_graph_delta,
2644 frontend,
2645 ctx,
2646 version,
2647 sketch_id,
2648 )
2649 .await
2650 {
2651 Ok((source_delta, scene_graph_delta)) => {
2652 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2653 last_result = Some((source_delta, scene_graph_delta.clone()));
2654 current_scene_graph_delta = scene_graph_delta;
2655 }
2656 Err(exec_err) => {
2657 crate::logln!(
2658 "Error executing circle-delete fallback trim operation: {}",
2659 exec_err
2660 );
2661 }
2662 }
2663
2664 let old_start_index = start_index;
2665 start_index = *next_index;
2666 if start_index <= old_start_index {
2667 start_index = old_start_index + 1;
2668 }
2669 continue;
2670 }
2671
2672 let old_start_index = start_index;
2673 start_index = *next_index;
2674 if start_index <= old_start_index {
2675 start_index = old_start_index + 1;
2676 }
2677 continue;
2678 }
2679 };
2680
2681 let trim_spawn_segment = current_scene_graph_delta
2683 .new_graph
2684 .objects
2685 .iter()
2686 .find(|obj| obj.id == *trim_spawn_seg_id)
2687 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
2688
2689 let plan = match build_trim_plan(
2690 *trim_spawn_seg_id,
2691 *trim_spawn_coords,
2692 trim_spawn_segment,
2693 &terminations.left_side,
2694 &terminations.right_side,
2695 ¤t_scene_graph_delta.new_graph.objects,
2696 default_unit,
2697 ) {
2698 Ok(plan) => plan,
2699 Err(e) => {
2700 crate::logln!("Error determining trim strategy: {}", e);
2701 let old_start_index = start_index;
2702 start_index = *next_index;
2703 if start_index <= old_start_index {
2704 start_index = old_start_index + 1;
2705 }
2706 continue;
2707 }
2708 };
2709 let strategy = lower_trim_plan(&plan);
2710
2711 let geometry_was_modified = trim_plan_modifies_geometry(&plan);
2714
2715 match execute_trim_operations_simple(
2717 strategy.clone(),
2718 ¤t_scene_graph_delta,
2719 frontend,
2720 ctx,
2721 version,
2722 sketch_id,
2723 )
2724 .await
2725 {
2726 Ok((source_delta, scene_graph_delta)) => {
2727 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2728 last_result = Some((source_delta, scene_graph_delta.clone()));
2729 current_scene_graph_delta = scene_graph_delta;
2730 }
2731 Err(e) => {
2732 crate::logln!("Error executing trim operations: {}", e);
2733 }
2734 }
2735
2736 let old_start_index = start_index;
2738 start_index = *next_index;
2739 if start_index <= old_start_index && !geometry_was_modified {
2740 start_index = old_start_index + 1;
2741 }
2742 }
2743 }
2744 }
2745
2746 if iteration_count >= max_iterations {
2747 return Err(format!("Reached max iterations ({})", max_iterations));
2748 }
2749
2750 let (source_delta, mut scene_graph_delta) =
2751 last_result.ok_or_else(|| "No trim operations were executed".to_string())?;
2752 scene_graph_delta.invalidates_ids = invalidates_ids;
2754 Ok((source_delta, scene_graph_delta))
2755}
2756
2757pub(crate) fn build_trim_plan(
2817 trim_spawn_id: ObjectId,
2818 trim_spawn_coords: Coords2d,
2819 trim_spawn_segment: &Object,
2820 left_side: &TrimTermination,
2821 right_side: &TrimTermination,
2822 objects: &[Object],
2823 default_unit: UnitLength,
2824) -> Result<TrimPlan, String> {
2825 if matches!(left_side, TrimTermination::SegEndPoint { .. })
2827 && matches!(right_side, TrimTermination::SegEndPoint { .. })
2828 {
2829 return Ok(TrimPlan::DeleteSegment {
2830 segment_id: trim_spawn_id,
2831 });
2832 }
2833
2834 let is_intersect_or_coincident = |side: &TrimTermination| -> bool {
2836 matches!(
2837 side,
2838 TrimTermination::Intersection { .. }
2839 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
2840 )
2841 };
2842
2843 let left_side_needs_tail_cut = is_intersect_or_coincident(left_side) && !is_intersect_or_coincident(right_side);
2844 let right_side_needs_tail_cut = is_intersect_or_coincident(right_side) && !is_intersect_or_coincident(left_side);
2845
2846 let ObjectKind::Segment { segment } = &trim_spawn_segment.kind else {
2848 return Err("Trim spawn segment is not a segment".to_string());
2849 };
2850
2851 let (_segment_type, ctor) = match segment {
2852 Segment::Line(line) => ("Line", &line.ctor),
2853 Segment::Arc(arc) => ("Arc", &arc.ctor),
2854 Segment::Circle(circle) => ("Circle", &circle.ctor),
2855 _ => {
2856 return Err("Trim spawn segment is not a Line, Arc, or Circle".to_string());
2857 }
2858 };
2859
2860 let units = match ctor {
2862 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
2863 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2864 _ => NumericSuffix::Mm,
2865 },
2866 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
2867 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2868 _ => NumericSuffix::Mm,
2869 },
2870 SegmentCtor::Circle(circle_ctor) => match &circle_ctor.start.x {
2871 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2872 _ => NumericSuffix::Mm,
2873 },
2874 _ => NumericSuffix::Mm,
2875 };
2876
2877 let find_distance_constraints_for_segment = |segment_id: ObjectId| -> Vec<ObjectId> {
2879 let mut constraint_ids = Vec::new();
2880 for obj in objects {
2881 let ObjectKind::Constraint { constraint } = &obj.kind else {
2882 continue;
2883 };
2884
2885 let Constraint::Distance(distance) = constraint else {
2886 continue;
2887 };
2888
2889 let points_owned_by_segment: Vec<bool> = distance
2895 .point_ids()
2896 .map(|point_id| {
2897 if let Some(point_obj) = objects.iter().find(|o| o.id == point_id)
2898 && let ObjectKind::Segment { segment } = &point_obj.kind
2899 && let Segment::Point(point) = segment
2900 && let Some(owner_id) = point.owner
2901 {
2902 return owner_id == segment_id;
2903 }
2904 false
2905 })
2906 .collect();
2907
2908 if points_owned_by_segment.len() == 2 && points_owned_by_segment.iter().all(|&owned| owned) {
2910 constraint_ids.push(obj.id);
2911 }
2912 }
2913 constraint_ids
2914 };
2915
2916 let find_existing_point_segment_coincident =
2918 |trim_seg_id: ObjectId, intersecting_seg_id: ObjectId| -> CoincidentData {
2919 let lookup_by_point_id = |point_id: ObjectId| -> Option<CoincidentData> {
2921 for obj in objects {
2922 let ObjectKind::Constraint { constraint } = &obj.kind else {
2923 continue;
2924 };
2925
2926 let Constraint::Coincident(coincident) = constraint else {
2927 continue;
2928 };
2929
2930 let involves_trim_seg = coincident.segment_ids().any(|id| id == trim_seg_id || id == point_id);
2931 let involves_point = coincident.contains_segment(point_id);
2932
2933 if involves_trim_seg && involves_point {
2934 return Some(CoincidentData {
2935 intersecting_seg_id,
2936 intersecting_endpoint_point_id: Some(point_id),
2937 existing_point_segment_constraint_id: Some(obj.id),
2938 });
2939 }
2940 }
2941 None
2942 };
2943
2944 let trim_seg = objects.iter().find(|obj| obj.id == trim_seg_id);
2946
2947 let mut trim_endpoint_ids: Vec<ObjectId> = Vec::new();
2948 if let Some(seg) = trim_seg
2949 && let ObjectKind::Segment { segment } = &seg.kind
2950 {
2951 match segment {
2952 Segment::Line(line) => {
2953 trim_endpoint_ids.push(line.start);
2954 trim_endpoint_ids.push(line.end);
2955 }
2956 Segment::Arc(arc) => {
2957 trim_endpoint_ids.push(arc.start);
2958 trim_endpoint_ids.push(arc.end);
2959 }
2960 _ => {}
2961 }
2962 }
2963
2964 let intersecting_obj = objects.iter().find(|obj| obj.id == intersecting_seg_id);
2965
2966 if let Some(obj) = intersecting_obj
2967 && let ObjectKind::Segment { segment } = &obj.kind
2968 && let Segment::Point(_) = segment
2969 && let Some(found) = lookup_by_point_id(intersecting_seg_id)
2970 {
2971 return found;
2972 }
2973
2974 let mut intersecting_endpoint_ids: Vec<ObjectId> = Vec::new();
2976 if let Some(obj) = intersecting_obj
2977 && let ObjectKind::Segment { segment } = &obj.kind
2978 {
2979 match segment {
2980 Segment::Line(line) => {
2981 intersecting_endpoint_ids.push(line.start);
2982 intersecting_endpoint_ids.push(line.end);
2983 }
2984 Segment::Arc(arc) => {
2985 intersecting_endpoint_ids.push(arc.start);
2986 intersecting_endpoint_ids.push(arc.end);
2987 }
2988 _ => {}
2989 }
2990 }
2991
2992 intersecting_endpoint_ids.push(intersecting_seg_id);
2994
2995 for obj in objects {
2997 let ObjectKind::Constraint { constraint } = &obj.kind else {
2998 continue;
2999 };
3000
3001 let Constraint::Coincident(coincident) = constraint else {
3002 continue;
3003 };
3004
3005 let constraint_segment_ids: Vec<ObjectId> = coincident.get_segments();
3006
3007 let involves_trim_seg = constraint_segment_ids.contains(&trim_seg_id)
3009 || trim_endpoint_ids.iter().any(|&id| constraint_segment_ids.contains(&id));
3010
3011 if !involves_trim_seg {
3012 continue;
3013 }
3014
3015 if let Some(&intersecting_endpoint_id) = intersecting_endpoint_ids
3017 .iter()
3018 .find(|&&id| constraint_segment_ids.contains(&id))
3019 {
3020 return CoincidentData {
3021 intersecting_seg_id,
3022 intersecting_endpoint_point_id: Some(intersecting_endpoint_id),
3023 existing_point_segment_constraint_id: Some(obj.id),
3024 };
3025 }
3026 }
3027
3028 CoincidentData {
3030 intersecting_seg_id,
3031 intersecting_endpoint_point_id: None,
3032 existing_point_segment_constraint_id: None,
3033 }
3034 };
3035
3036 let find_point_segment_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<serde_json::Value> {
3038 let mut constraints: Vec<serde_json::Value> = Vec::new();
3039 for obj in objects {
3040 let ObjectKind::Constraint { constraint } = &obj.kind else {
3041 continue;
3042 };
3043
3044 let Constraint::Coincident(coincident) = constraint else {
3045 continue;
3046 };
3047
3048 if !coincident.contains_segment(endpoint_point_id) {
3050 continue;
3051 }
3052
3053 let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
3055
3056 if let Some(other_id) = other_segment_id
3057 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
3058 {
3059 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
3061 constraints.push(serde_json::json!({
3062 "constraintId": obj.id.0,
3063 "segmentOrPointId": other_id.0,
3064 }));
3065 }
3066 }
3067 }
3068 constraints
3069 };
3070
3071 let find_point_point_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
3074 let mut constraint_ids = Vec::new();
3075 for obj in objects {
3076 let ObjectKind::Constraint { constraint } = &obj.kind else {
3077 continue;
3078 };
3079
3080 let Constraint::Coincident(coincident) = constraint else {
3081 continue;
3082 };
3083
3084 if !coincident.contains_segment(endpoint_point_id) {
3086 continue;
3087 }
3088
3089 let is_point_point = coincident.segment_ids().all(|seg_id| {
3091 if let Some(seg_obj) = objects.iter().find(|o| o.id == seg_id) {
3092 matches!(&seg_obj.kind, ObjectKind::Segment { segment } if matches!(segment, Segment::Point(_)))
3093 } else {
3094 false
3095 }
3096 });
3097
3098 if is_point_point {
3099 constraint_ids.push(obj.id);
3100 }
3101 }
3102 constraint_ids
3103 };
3104
3105 let find_point_segment_coincident_constraint_ids = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
3108 let mut constraint_ids = Vec::new();
3109 for obj in objects {
3110 let ObjectKind::Constraint { constraint } = &obj.kind else {
3111 continue;
3112 };
3113
3114 let Constraint::Coincident(coincident) = constraint else {
3115 continue;
3116 };
3117
3118 if !coincident.contains_segment(endpoint_point_id) {
3120 continue;
3121 }
3122
3123 let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
3125
3126 if let Some(other_id) = other_segment_id
3127 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
3128 {
3129 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
3131 constraint_ids.push(obj.id);
3132 }
3133 }
3134 }
3135 constraint_ids
3136 };
3137
3138 if left_side_needs_tail_cut || right_side_needs_tail_cut {
3140 let side = if left_side_needs_tail_cut {
3141 left_side
3142 } else {
3143 right_side
3144 };
3145
3146 let intersection_coords = match side {
3147 TrimTermination::Intersection {
3148 trim_termination_coords,
3149 ..
3150 }
3151 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3152 trim_termination_coords,
3153 ..
3154 } => *trim_termination_coords,
3155 TrimTermination::SegEndPoint { .. } => {
3156 return Err("Logic error: side should not be segEndPoint here".to_string());
3157 }
3158 };
3159
3160 let endpoint_to_change = if left_side_needs_tail_cut {
3161 EndpointChanged::End
3162 } else {
3163 EndpointChanged::Start
3164 };
3165
3166 let intersecting_seg_id = match side {
3167 TrimTermination::Intersection {
3168 intersecting_seg_id, ..
3169 }
3170 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3171 intersecting_seg_id, ..
3172 } => *intersecting_seg_id,
3173 TrimTermination::SegEndPoint { .. } => {
3174 return Err("Logic error".to_string());
3175 }
3176 };
3177
3178 let mut coincident_data = if matches!(
3179 side,
3180 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3181 ) {
3182 let point_id = match side {
3183 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3184 other_segment_point_id, ..
3185 } => *other_segment_point_id,
3186 _ => return Err("Logic error".to_string()),
3187 };
3188 let mut data = find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id);
3189 data.intersecting_endpoint_point_id = Some(point_id);
3190 data
3191 } else {
3192 find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id)
3193 };
3194
3195 let trim_seg = objects.iter().find(|obj| obj.id == trim_spawn_id);
3197
3198 let endpoint_point_id = if let Some(seg) = trim_seg {
3199 let ObjectKind::Segment { segment } = &seg.kind else {
3200 return Err("Trim spawn segment is not a segment".to_string());
3201 };
3202 match segment {
3203 Segment::Line(line) => {
3204 if endpoint_to_change == EndpointChanged::Start {
3205 Some(line.start)
3206 } else {
3207 Some(line.end)
3208 }
3209 }
3210 Segment::Arc(arc) => {
3211 if endpoint_to_change == EndpointChanged::Start {
3212 Some(arc.start)
3213 } else {
3214 Some(arc.end)
3215 }
3216 }
3217 _ => None,
3218 }
3219 } else {
3220 None
3221 };
3222
3223 if let (Some(endpoint_id), Some(existing_constraint_id)) =
3224 (endpoint_point_id, coincident_data.existing_point_segment_constraint_id)
3225 {
3226 let constraint_involves_trimmed_endpoint = objects
3227 .iter()
3228 .find(|obj| obj.id == existing_constraint_id)
3229 .and_then(|obj| match &obj.kind {
3230 ObjectKind::Constraint {
3231 constraint: Constraint::Coincident(coincident),
3232 } => Some(coincident.contains_segment(endpoint_id) || coincident.contains_segment(trim_spawn_id)),
3233 _ => None,
3234 })
3235 .unwrap_or(false);
3236
3237 if !constraint_involves_trimmed_endpoint {
3238 coincident_data.existing_point_segment_constraint_id = None;
3239 coincident_data.intersecting_endpoint_point_id = None;
3240 }
3241 }
3242
3243 let coincident_end_constraint_to_delete_ids = if let Some(point_id) = endpoint_point_id {
3245 let mut constraint_ids = find_point_point_coincident_constraints(point_id);
3246 constraint_ids.extend(find_point_segment_coincident_constraint_ids(point_id));
3248 constraint_ids
3249 } else {
3250 Vec::new()
3251 };
3252
3253 let new_ctor = match ctor {
3255 SegmentCtor::Line(line_ctor) => {
3256 let new_point = crate::frontend::sketch::Point2d {
3258 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
3259 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
3260 };
3261 if endpoint_to_change == EndpointChanged::Start {
3262 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3263 start: new_point,
3264 end: line_ctor.end.clone(),
3265 construction: line_ctor.construction,
3266 })
3267 } else {
3268 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3269 start: line_ctor.start.clone(),
3270 end: new_point,
3271 construction: line_ctor.construction,
3272 })
3273 }
3274 }
3275 SegmentCtor::Arc(arc_ctor) => {
3276 let new_point = crate::frontend::sketch::Point2d {
3278 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
3279 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
3280 };
3281 if endpoint_to_change == EndpointChanged::Start {
3282 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3283 start: new_point,
3284 end: arc_ctor.end.clone(),
3285 center: arc_ctor.center.clone(),
3286 construction: arc_ctor.construction,
3287 })
3288 } else {
3289 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3290 start: arc_ctor.start.clone(),
3291 end: new_point,
3292 center: arc_ctor.center.clone(),
3293 construction: arc_ctor.construction,
3294 })
3295 }
3296 }
3297 _ => {
3298 return Err("Unsupported segment type for edit".to_string());
3299 }
3300 };
3301
3302 let mut all_constraint_ids_to_delete: Vec<ObjectId> = Vec::new();
3304 if let Some(constraint_id) = coincident_data.existing_point_segment_constraint_id {
3305 all_constraint_ids_to_delete.push(constraint_id);
3306 }
3307 all_constraint_ids_to_delete.extend(coincident_end_constraint_to_delete_ids);
3308
3309 let distance_constraint_ids = find_distance_constraints_for_segment(trim_spawn_id);
3312 all_constraint_ids_to_delete.extend(distance_constraint_ids);
3313
3314 return Ok(TrimPlan::TailCut {
3315 segment_id: trim_spawn_id,
3316 endpoint_changed: endpoint_to_change,
3317 ctor: new_ctor,
3318 segment_or_point_to_make_coincident_to: intersecting_seg_id,
3319 intersecting_endpoint_point_id: coincident_data.intersecting_endpoint_point_id,
3320 constraint_ids_to_delete: all_constraint_ids_to_delete,
3321 });
3322 }
3323
3324 if matches!(segment, Segment::Circle(_)) {
3327 let left_side_intersects = is_intersect_or_coincident(left_side);
3328 let right_side_intersects = is_intersect_or_coincident(right_side);
3329 if !(left_side_intersects && right_side_intersects) {
3330 return Err(format!(
3331 "Unsupported circle trim termination combination: left={:?} right={:?}",
3332 left_side, right_side
3333 ));
3334 }
3335
3336 let left_trim_coords = match left_side {
3337 TrimTermination::SegEndPoint {
3338 trim_termination_coords,
3339 }
3340 | TrimTermination::Intersection {
3341 trim_termination_coords,
3342 ..
3343 }
3344 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3345 trim_termination_coords,
3346 ..
3347 } => *trim_termination_coords,
3348 };
3349 let right_trim_coords = match right_side {
3350 TrimTermination::SegEndPoint {
3351 trim_termination_coords,
3352 }
3353 | TrimTermination::Intersection {
3354 trim_termination_coords,
3355 ..
3356 }
3357 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3358 trim_termination_coords,
3359 ..
3360 } => *trim_termination_coords,
3361 };
3362
3363 let trim_points_coincident = ((left_trim_coords.x - right_trim_coords.x)
3366 * (left_trim_coords.x - right_trim_coords.x)
3367 + (left_trim_coords.y - right_trim_coords.y) * (left_trim_coords.y - right_trim_coords.y))
3368 .sqrt()
3369 <= EPSILON_POINT_ON_SEGMENT * 10.0;
3370 if trim_points_coincident {
3371 return Ok(TrimPlan::DeleteSegment {
3372 segment_id: trim_spawn_id,
3373 });
3374 }
3375
3376 let circle_center_coords =
3377 get_position_coords_from_circle(trim_spawn_segment, CirclePoint::Center, objects, default_unit)
3378 .ok_or_else(|| {
3379 format!(
3380 "Could not get center coordinates for circle segment {}",
3381 trim_spawn_id.0
3382 )
3383 })?;
3384
3385 let spawn_on_left_to_right = is_point_on_arc(
3387 trim_spawn_coords,
3388 circle_center_coords,
3389 left_trim_coords,
3390 right_trim_coords,
3391 EPSILON_POINT_ON_SEGMENT,
3392 );
3393 let (arc_start_coords, arc_end_coords, arc_start_termination, arc_end_termination) = if spawn_on_left_to_right {
3394 (
3395 right_trim_coords,
3396 left_trim_coords,
3397 Box::new(right_side.clone()),
3398 Box::new(left_side.clone()),
3399 )
3400 } else {
3401 (
3402 left_trim_coords,
3403 right_trim_coords,
3404 Box::new(left_side.clone()),
3405 Box::new(right_side.clone()),
3406 )
3407 };
3408
3409 return Ok(TrimPlan::ReplaceCircleWithArc {
3410 circle_id: trim_spawn_id,
3411 arc_start_coords,
3412 arc_end_coords,
3413 arc_start_termination,
3414 arc_end_termination,
3415 });
3416 }
3417
3418 let left_side_intersects = is_intersect_or_coincident(left_side);
3420 let right_side_intersects = is_intersect_or_coincident(right_side);
3421
3422 if left_side_intersects && right_side_intersects {
3423 let left_intersecting_seg_id = match left_side {
3426 TrimTermination::Intersection {
3427 intersecting_seg_id, ..
3428 }
3429 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3430 intersecting_seg_id, ..
3431 } => *intersecting_seg_id,
3432 TrimTermination::SegEndPoint { .. } => {
3433 return Err("Logic error: left side should not be segEndPoint".to_string());
3434 }
3435 };
3436
3437 let right_intersecting_seg_id = match right_side {
3438 TrimTermination::Intersection {
3439 intersecting_seg_id, ..
3440 }
3441 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3442 intersecting_seg_id, ..
3443 } => *intersecting_seg_id,
3444 TrimTermination::SegEndPoint { .. } => {
3445 return Err("Logic error: right side should not be segEndPoint".to_string());
3446 }
3447 };
3448
3449 let left_coincident_data = if matches!(
3450 left_side,
3451 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3452 ) {
3453 let point_id = match left_side {
3454 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3455 other_segment_point_id, ..
3456 } => *other_segment_point_id,
3457 _ => return Err("Logic error".to_string()),
3458 };
3459 let mut data = find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id);
3460 data.intersecting_endpoint_point_id = Some(point_id);
3461 data
3462 } else {
3463 find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id)
3464 };
3465
3466 let right_coincident_data = if matches!(
3467 right_side,
3468 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3469 ) {
3470 let point_id = match right_side {
3471 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3472 other_segment_point_id, ..
3473 } => *other_segment_point_id,
3474 _ => return Err("Logic error".to_string()),
3475 };
3476 let mut data = find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id);
3477 data.intersecting_endpoint_point_id = Some(point_id);
3478 data
3479 } else {
3480 find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id)
3481 };
3482
3483 let (original_start_point_id, original_end_point_id) = match segment {
3485 Segment::Line(line) => (Some(line.start), Some(line.end)),
3486 Segment::Arc(arc) => (Some(arc.start), Some(arc.end)),
3487 _ => (None, None),
3488 };
3489
3490 let original_end_point_coords = match segment {
3492 Segment::Line(_) => {
3493 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
3494 }
3495 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
3496 _ => None,
3497 };
3498
3499 let Some(original_end_coords) = original_end_point_coords else {
3500 return Err(
3501 "Could not get original end point coordinates before editing - this is required for split trim"
3502 .to_string(),
3503 );
3504 };
3505
3506 let left_trim_coords = match left_side {
3508 TrimTermination::SegEndPoint {
3509 trim_termination_coords,
3510 }
3511 | TrimTermination::Intersection {
3512 trim_termination_coords,
3513 ..
3514 }
3515 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3516 trim_termination_coords,
3517 ..
3518 } => *trim_termination_coords,
3519 };
3520
3521 let right_trim_coords = match right_side {
3522 TrimTermination::SegEndPoint {
3523 trim_termination_coords,
3524 }
3525 | TrimTermination::Intersection {
3526 trim_termination_coords,
3527 ..
3528 }
3529 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3530 trim_termination_coords,
3531 ..
3532 } => *trim_termination_coords,
3533 };
3534
3535 let dist_to_original_end = ((right_trim_coords.x - original_end_coords.x)
3537 * (right_trim_coords.x - original_end_coords.x)
3538 + (right_trim_coords.y - original_end_coords.y) * (right_trim_coords.y - original_end_coords.y))
3539 .sqrt();
3540 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3541 return Err(
3542 "Split point is at original end point - this should be handled as cutTail, not split".to_string(),
3543 );
3544 }
3545
3546 let mut constraints_to_migrate: Vec<ConstraintToMigrate> = Vec::new();
3549 let mut constraints_to_delete_set: IndexSet<ObjectId> = IndexSet::new();
3550
3551 if let Some(constraint_id) = left_coincident_data.existing_point_segment_constraint_id {
3553 constraints_to_delete_set.insert(constraint_id);
3554 }
3555 if let Some(constraint_id) = right_coincident_data.existing_point_segment_constraint_id {
3556 constraints_to_delete_set.insert(constraint_id);
3557 }
3558
3559 if let Some(end_id) = original_end_point_id {
3561 let end_point_point_constraint_ids = find_point_point_coincident_constraints(end_id);
3562 for constraint_id in end_point_point_constraint_ids {
3563 let other_point_id_opt = objects.iter().find_map(|obj| {
3565 if obj.id != constraint_id {
3566 return None;
3567 }
3568 let ObjectKind::Constraint { constraint } = &obj.kind else {
3569 return None;
3570 };
3571 let Constraint::Coincident(coincident) = constraint else {
3572 return None;
3573 };
3574 coincident.segment_ids().find(|&seg_id| seg_id != end_id)
3575 });
3576
3577 if let Some(other_point_id) = other_point_id_opt {
3578 constraints_to_delete_set.insert(constraint_id);
3579 constraints_to_migrate.push(ConstraintToMigrate {
3581 constraint_id,
3582 other_entity_id: other_point_id,
3583 is_point_point: true,
3584 attach_to_endpoint: AttachToEndpoint::End,
3585 });
3586 }
3587 }
3588 }
3589
3590 if let Some(end_id) = original_end_point_id {
3592 let end_point_segment_constraints = find_point_segment_coincident_constraints(end_id);
3593 for constraint_json in end_point_segment_constraints {
3594 if let Some(constraint_id_usize) = constraint_json
3595 .get("constraintId")
3596 .and_then(|v| v.as_u64())
3597 .map(|id| id as usize)
3598 {
3599 let constraint_id = ObjectId(constraint_id_usize);
3600 constraints_to_delete_set.insert(constraint_id);
3601 if let Some(other_id_usize) = constraint_json
3603 .get("segmentOrPointId")
3604 .and_then(|v| v.as_u64())
3605 .map(|id| id as usize)
3606 {
3607 constraints_to_migrate.push(ConstraintToMigrate {
3608 constraint_id,
3609 other_entity_id: ObjectId(other_id_usize),
3610 is_point_point: false,
3611 attach_to_endpoint: AttachToEndpoint::End,
3612 });
3613 }
3614 }
3615 }
3616 }
3617
3618 if let Some(end_id) = original_end_point_id {
3623 for obj in objects {
3624 let ObjectKind::Constraint { constraint } = &obj.kind else {
3625 continue;
3626 };
3627
3628 let Constraint::Coincident(coincident) = constraint else {
3629 continue;
3630 };
3631
3632 if !coincident.contains_segment(trim_spawn_id) {
3637 continue;
3638 }
3639 if let (Some(start_id), Some(end_id_val)) = (original_start_point_id, Some(end_id))
3642 && coincident.segment_ids().any(|id| id == start_id || id == end_id_val)
3643 {
3644 continue; }
3646
3647 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
3649
3650 if let Some(other_id) = other_id {
3651 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3653 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3654 continue;
3655 };
3656
3657 let Segment::Point(point) = other_segment else {
3658 continue;
3659 };
3660
3661 let point_coords = Coords2d {
3663 x: number_to_unit(&point.position.x, default_unit),
3664 y: number_to_unit(&point.position.y, default_unit),
3665 };
3666
3667 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
3670 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
3671 if let ObjectKind::Segment {
3672 segment: Segment::Point(end_point),
3673 } = &end_point_obj.kind
3674 {
3675 Some(Coords2d {
3676 x: number_to_unit(&end_point.position.x, default_unit),
3677 y: number_to_unit(&end_point.position.y, default_unit),
3678 })
3679 } else {
3680 None
3681 }
3682 } else {
3683 None
3684 }
3685 } else {
3686 None
3687 };
3688
3689 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
3690 let dist_to_original_end = ((point_coords.x - reference_coords.x)
3691 * (point_coords.x - reference_coords.x)
3692 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
3693 .sqrt();
3694
3695 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3696 let has_point_point_constraint = find_point_point_coincident_constraints(end_id)
3699 .iter()
3700 .any(|&constraint_id| {
3701 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
3702 if let ObjectKind::Constraint {
3703 constraint: Constraint::Coincident(coincident),
3704 } = &constraint_obj.kind
3705 {
3706 coincident.contains_segment(other_id)
3707 } else {
3708 false
3709 }
3710 } else {
3711 false
3712 }
3713 });
3714
3715 if !has_point_point_constraint {
3716 constraints_to_migrate.push(ConstraintToMigrate {
3718 constraint_id: obj.id,
3719 other_entity_id: other_id,
3720 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
3723 }
3724 constraints_to_delete_set.insert(obj.id);
3726 }
3727 }
3728 }
3729 }
3730 }
3731
3732 let split_point = right_trim_coords; let segment_start_coords = match segment {
3737 Segment::Line(_) => {
3738 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::Start, objects, default_unit)
3739 }
3740 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Start, objects, default_unit),
3741 _ => None,
3742 };
3743 let segment_end_coords = match segment {
3744 Segment::Line(_) => {
3745 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
3746 }
3747 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
3748 _ => None,
3749 };
3750 let segment_center_coords = match segment {
3751 Segment::Line(_) => None,
3752 Segment::Arc(_) => {
3753 get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Center, objects, default_unit)
3754 }
3755 _ => None,
3756 };
3757
3758 if let (Some(start_coords), Some(end_coords)) = (segment_start_coords, segment_end_coords) {
3759 let split_point_t_opt = match segment {
3761 Segment::Line(_) => Some(project_point_onto_segment(split_point, start_coords, end_coords)),
3762 Segment::Arc(_) => segment_center_coords
3763 .map(|center| project_point_onto_arc(split_point, center, start_coords, end_coords)),
3764 _ => None,
3765 };
3766
3767 if let Some(split_point_t) = split_point_t_opt {
3768 for obj in objects {
3770 let ObjectKind::Constraint { constraint } = &obj.kind else {
3771 continue;
3772 };
3773
3774 let Constraint::Coincident(coincident) = constraint else {
3775 continue;
3776 };
3777
3778 if !coincident.contains_segment(trim_spawn_id) {
3780 continue;
3781 }
3782
3783 if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id)
3785 && coincident.segment_ids().any(|id| id == start_id || id == end_id)
3786 {
3787 continue;
3788 }
3789
3790 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
3792
3793 if let Some(other_id) = other_id {
3794 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3796 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3797 continue;
3798 };
3799
3800 let Segment::Point(point) = other_segment else {
3801 continue;
3802 };
3803
3804 let point_coords = Coords2d {
3806 x: number_to_unit(&point.position.x, default_unit),
3807 y: number_to_unit(&point.position.y, default_unit),
3808 };
3809
3810 let point_t = match segment {
3812 Segment::Line(_) => project_point_onto_segment(point_coords, start_coords, end_coords),
3813 Segment::Arc(_) => {
3814 if let Some(center) = segment_center_coords {
3815 project_point_onto_arc(point_coords, center, start_coords, end_coords)
3816 } else {
3817 continue; }
3819 }
3820 _ => continue, };
3822
3823 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
3826 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
3827 if let ObjectKind::Segment {
3828 segment: Segment::Point(end_point),
3829 } = &end_point_obj.kind
3830 {
3831 Some(Coords2d {
3832 x: number_to_unit(&end_point.position.x, default_unit),
3833 y: number_to_unit(&end_point.position.y, default_unit),
3834 })
3835 } else {
3836 None
3837 }
3838 } else {
3839 None
3840 }
3841 } else {
3842 None
3843 };
3844
3845 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
3846 let dist_to_original_end = ((point_coords.x - reference_coords.x)
3847 * (point_coords.x - reference_coords.x)
3848 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
3849 .sqrt();
3850
3851 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3852 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
3856 find_point_point_coincident_constraints(end_id)
3857 .iter()
3858 .any(|&constraint_id| {
3859 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
3860 {
3861 if let ObjectKind::Constraint {
3862 constraint: Constraint::Coincident(coincident),
3863 } = &constraint_obj.kind
3864 {
3865 coincident.contains_segment(other_id)
3866 } else {
3867 false
3868 }
3869 } else {
3870 false
3871 }
3872 })
3873 } else {
3874 false
3875 };
3876
3877 if !has_point_point_constraint {
3878 constraints_to_migrate.push(ConstraintToMigrate {
3880 constraint_id: obj.id,
3881 other_entity_id: other_id,
3882 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
3885 }
3886 constraints_to_delete_set.insert(obj.id);
3888 continue; }
3890
3891 let dist_to_start = ((point_coords.x - start_coords.x) * (point_coords.x - start_coords.x)
3893 + (point_coords.y - start_coords.y) * (point_coords.y - start_coords.y))
3894 .sqrt();
3895 let is_at_start = (point_t - 0.0).abs() < EPSILON_POINT_ON_SEGMENT
3896 || dist_to_start < EPSILON_POINT_ON_SEGMENT;
3897
3898 if is_at_start {
3899 continue; }
3901
3902 let dist_to_split = (point_t - split_point_t).abs();
3904 if dist_to_split < EPSILON_POINT_ON_SEGMENT * 100.0 {
3905 continue; }
3907
3908 if point_t > split_point_t {
3910 constraints_to_migrate.push(ConstraintToMigrate {
3911 constraint_id: obj.id,
3912 other_entity_id: other_id,
3913 is_point_point: false, attach_to_endpoint: AttachToEndpoint::Segment, });
3916 constraints_to_delete_set.insert(obj.id);
3917 }
3918 }
3919 }
3920 }
3921 } } let distance_constraint_ids_for_split = find_distance_constraints_for_segment(trim_spawn_id);
3929
3930 let arc_center_point_id: Option<ObjectId> = match segment {
3932 Segment::Arc(arc) => Some(arc.center),
3933 _ => None,
3934 };
3935
3936 for constraint_id in distance_constraint_ids_for_split {
3937 if let Some(center_id) = arc_center_point_id {
3939 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
3941 && let ObjectKind::Constraint { constraint } = &constraint_obj.kind
3942 && let Constraint::Distance(distance) = constraint
3943 && distance.contains_point(center_id)
3944 {
3945 continue;
3947 }
3948 }
3949
3950 constraints_to_delete_set.insert(constraint_id);
3951 }
3952
3953 for obj in objects {
3961 let ObjectKind::Constraint { constraint } = &obj.kind else {
3962 continue;
3963 };
3964
3965 let Constraint::Coincident(coincident) = constraint else {
3966 continue;
3967 };
3968
3969 if !coincident.contains_segment(trim_spawn_id) {
3971 continue;
3972 }
3973
3974 if constraints_to_delete_set.contains(&obj.id) {
3976 continue;
3977 }
3978
3979 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
3986
3987 if let Some(other_id) = other_id {
3988 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3990 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3991 continue;
3992 };
3993
3994 let Segment::Point(point) = other_segment else {
3995 continue;
3996 };
3997
3998 let _is_endpoint_constraint =
4001 if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id) {
4002 coincident.segment_ids().any(|id| id == start_id || id == end_id)
4003 } else {
4004 false
4005 };
4006
4007 let point_coords = Coords2d {
4009 x: number_to_unit(&point.position.x, default_unit),
4010 y: number_to_unit(&point.position.y, default_unit),
4011 };
4012
4013 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
4015 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
4016 if let ObjectKind::Segment {
4017 segment: Segment::Point(end_point),
4018 } = &end_point_obj.kind
4019 {
4020 Some(Coords2d {
4021 x: number_to_unit(&end_point.position.x, default_unit),
4022 y: number_to_unit(&end_point.position.y, default_unit),
4023 })
4024 } else {
4025 None
4026 }
4027 } else {
4028 None
4029 }
4030 } else {
4031 None
4032 };
4033
4034 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
4035 let dist_to_original_end = ((point_coords.x - reference_coords.x)
4036 * (point_coords.x - reference_coords.x)
4037 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
4038 .sqrt();
4039
4040 let is_at_original_end = dist_to_original_end < EPSILON_POINT_ON_SEGMENT * 2.0;
4043
4044 if is_at_original_end {
4045 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
4048 find_point_point_coincident_constraints(end_id)
4049 .iter()
4050 .any(|&constraint_id| {
4051 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
4052 if let ObjectKind::Constraint {
4053 constraint: Constraint::Coincident(coincident),
4054 } = &constraint_obj.kind
4055 {
4056 coincident.contains_segment(other_id)
4057 } else {
4058 false
4059 }
4060 } else {
4061 false
4062 }
4063 })
4064 } else {
4065 false
4066 };
4067
4068 if !has_point_point_constraint {
4069 constraints_to_migrate.push(ConstraintToMigrate {
4071 constraint_id: obj.id,
4072 other_entity_id: other_id,
4073 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
4076 }
4077 constraints_to_delete_set.insert(obj.id);
4079 }
4080 }
4081 }
4082 }
4083
4084 let constraints_to_delete: Vec<ObjectId> = constraints_to_delete_set.iter().copied().collect();
4086 let plan = TrimPlan::SplitSegment {
4087 segment_id: trim_spawn_id,
4088 left_trim_coords,
4089 right_trim_coords,
4090 original_end_coords,
4091 left_side: Box::new(left_side.clone()),
4092 right_side: Box::new(right_side.clone()),
4093 left_side_coincident_data: CoincidentData {
4094 intersecting_seg_id: left_intersecting_seg_id,
4095 intersecting_endpoint_point_id: left_coincident_data.intersecting_endpoint_point_id,
4096 existing_point_segment_constraint_id: left_coincident_data.existing_point_segment_constraint_id,
4097 },
4098 right_side_coincident_data: CoincidentData {
4099 intersecting_seg_id: right_intersecting_seg_id,
4100 intersecting_endpoint_point_id: right_coincident_data.intersecting_endpoint_point_id,
4101 existing_point_segment_constraint_id: right_coincident_data.existing_point_segment_constraint_id,
4102 },
4103 constraints_to_migrate,
4104 constraints_to_delete,
4105 };
4106
4107 return Ok(plan);
4108 }
4109
4110 Err(format!(
4115 "Unsupported trim termination combination: left={:?} right={:?}",
4116 left_side, right_side
4117 ))
4118}
4119
4120pub(crate) async fn execute_trim_operations_simple(
4132 strategy: Vec<TrimOperation>,
4133 current_scene_graph_delta: &crate::frontend::api::SceneGraphDelta,
4134 frontend: &mut crate::frontend::FrontendState,
4135 ctx: &crate::ExecutorContext,
4136 version: crate::frontend::api::Version,
4137 sketch_id: ObjectId,
4138) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
4139 use crate::frontend::SketchApi;
4140 use crate::frontend::sketch::Constraint;
4141 use crate::frontend::sketch::ExistingSegmentCtor;
4142 use crate::frontend::sketch::SegmentCtor;
4143
4144 let default_unit = frontend.default_length_unit();
4145
4146 let mut op_index = 0;
4147 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = None;
4148 let mut invalidates_ids = false;
4149
4150 while op_index < strategy.len() {
4151 let mut consumed_ops = 1;
4152 let operation_result = match &strategy[op_index] {
4153 TrimOperation::SimpleTrim { segment_to_trim_id } => {
4154 frontend
4156 .delete_objects(
4157 ctx,
4158 version,
4159 sketch_id,
4160 Vec::new(), vec![*segment_to_trim_id], )
4163 .await
4164 .map_err(|e| format!("Failed to delete segment: {}", e.error.message()))
4165 }
4166 TrimOperation::EditSegment {
4167 segment_id,
4168 ctor,
4169 endpoint_changed,
4170 } => {
4171 if op_index + 1 < strategy.len() {
4174 if let TrimOperation::AddCoincidentConstraint {
4175 segment_id: coincident_seg_id,
4176 endpoint_changed: coincident_endpoint_changed,
4177 segment_or_point_to_make_coincident_to,
4178 intersecting_endpoint_point_id,
4179 } = &strategy[op_index + 1]
4180 {
4181 if segment_id == coincident_seg_id && endpoint_changed == coincident_endpoint_changed {
4182 let mut delete_constraint_ids: Vec<ObjectId> = Vec::new();
4184 consumed_ops = 2;
4185
4186 if op_index + 2 < strategy.len()
4187 && let TrimOperation::DeleteConstraints { constraint_ids } = &strategy[op_index + 2]
4188 {
4189 delete_constraint_ids = constraint_ids.to_vec();
4190 consumed_ops = 3;
4191 }
4192
4193 let segment_ctor = ctor.clone();
4195
4196 let edited_segment = current_scene_graph_delta
4198 .new_graph
4199 .objects
4200 .iter()
4201 .find(|obj| obj.id == *segment_id)
4202 .ok_or_else(|| format!("Failed to find segment {} for tail-cut batch", segment_id.0))?;
4203
4204 let endpoint_point_id = match &edited_segment.kind {
4205 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4206 crate::frontend::sketch::Segment::Line(line) => {
4207 if *endpoint_changed == EndpointChanged::Start {
4208 line.start
4209 } else {
4210 line.end
4211 }
4212 }
4213 crate::frontend::sketch::Segment::Arc(arc) => {
4214 if *endpoint_changed == EndpointChanged::Start {
4215 arc.start
4216 } else {
4217 arc.end
4218 }
4219 }
4220 _ => {
4221 return Err("Unsupported segment type for tail-cut batch".to_string());
4222 }
4223 },
4224 _ => {
4225 return Err("Edited object is not a segment (tail-cut batch)".to_string());
4226 }
4227 };
4228
4229 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
4230 vec![endpoint_point_id.into(), (*point_id).into()]
4231 } else {
4232 vec![
4233 endpoint_point_id.into(),
4234 (*segment_or_point_to_make_coincident_to).into(),
4235 ]
4236 };
4237
4238 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4239 segments: coincident_segments,
4240 });
4241
4242 let segment_to_edit = ExistingSegmentCtor {
4243 id: *segment_id,
4244 ctor: segment_ctor,
4245 };
4246
4247 frontend
4250 .batch_tail_cut_operations(
4251 ctx,
4252 version,
4253 sketch_id,
4254 vec![segment_to_edit],
4255 vec![constraint],
4256 delete_constraint_ids,
4257 )
4258 .await
4259 .map_err(|e| format!("Failed to batch tail-cut operations: {}", e.error.message()))
4260 } else {
4261 let segment_to_edit = ExistingSegmentCtor {
4263 id: *segment_id,
4264 ctor: ctor.clone(),
4265 };
4266
4267 frontend
4268 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4269 .await
4270 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4271 }
4272 } else {
4273 let segment_to_edit = ExistingSegmentCtor {
4275 id: *segment_id,
4276 ctor: ctor.clone(),
4277 };
4278
4279 frontend
4280 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4281 .await
4282 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4283 }
4284 } else {
4285 let segment_to_edit = ExistingSegmentCtor {
4287 id: *segment_id,
4288 ctor: ctor.clone(),
4289 };
4290
4291 frontend
4292 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4293 .await
4294 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4295 }
4296 }
4297 TrimOperation::AddCoincidentConstraint {
4298 segment_id,
4299 endpoint_changed,
4300 segment_or_point_to_make_coincident_to,
4301 intersecting_endpoint_point_id,
4302 } => {
4303 let edited_segment = current_scene_graph_delta
4305 .new_graph
4306 .objects
4307 .iter()
4308 .find(|obj| obj.id == *segment_id)
4309 .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
4310
4311 let new_segment_endpoint_point_id = match &edited_segment.kind {
4313 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4314 crate::frontend::sketch::Segment::Line(line) => {
4315 if *endpoint_changed == EndpointChanged::Start {
4316 line.start
4317 } else {
4318 line.end
4319 }
4320 }
4321 crate::frontend::sketch::Segment::Arc(arc) => {
4322 if *endpoint_changed == EndpointChanged::Start {
4323 arc.start
4324 } else {
4325 arc.end
4326 }
4327 }
4328 _ => {
4329 return Err("Unsupported segment type for addCoincidentConstraint".to_string());
4330 }
4331 },
4332 _ => {
4333 return Err("Edited object is not a segment".to_string());
4334 }
4335 };
4336
4337 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
4339 vec![new_segment_endpoint_point_id.into(), (*point_id).into()]
4340 } else {
4341 vec![
4342 new_segment_endpoint_point_id.into(),
4343 (*segment_or_point_to_make_coincident_to).into(),
4344 ]
4345 };
4346
4347 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4348 segments: coincident_segments,
4349 });
4350
4351 frontend
4352 .add_constraint(ctx, version, sketch_id, constraint)
4353 .await
4354 .map_err(|e| format!("Failed to add constraint: {}", e.error.message()))
4355 }
4356 TrimOperation::DeleteConstraints { constraint_ids } => {
4357 let constraint_object_ids: Vec<ObjectId> = constraint_ids.to_vec();
4359
4360 frontend
4361 .delete_objects(
4362 ctx,
4363 version,
4364 sketch_id,
4365 constraint_object_ids,
4366 Vec::new(), )
4368 .await
4369 .map_err(|e| format!("Failed to delete constraints: {}", e.error.message()))
4370 }
4371 TrimOperation::ReplaceCircleWithArc {
4372 circle_id,
4373 arc_start_coords,
4374 arc_end_coords,
4375 arc_start_termination,
4376 arc_end_termination,
4377 } => {
4378 let original_circle = current_scene_graph_delta
4380 .new_graph
4381 .objects
4382 .iter()
4383 .find(|obj| obj.id == *circle_id)
4384 .ok_or_else(|| format!("Failed to find original circle {}", circle_id.0))?;
4385
4386 let (original_circle_start_id, original_circle_center_id, circle_ctor) = match &original_circle.kind {
4387 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4388 crate::frontend::sketch::Segment::Circle(circle) => match &circle.ctor {
4389 SegmentCtor::Circle(circle_ctor) => (circle.start, circle.center, circle_ctor.clone()),
4390 _ => return Err("Circle does not have a Circle ctor".to_string()),
4391 },
4392 _ => return Err("Original segment is not a circle".to_string()),
4393 },
4394 _ => return Err("Original object is not a segment".to_string()),
4395 };
4396
4397 let units = match &circle_ctor.start.x {
4398 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4399 _ => crate::pretty::NumericSuffix::Mm,
4400 };
4401
4402 let coords_to_point_expr = |coords: Coords2d| crate::frontend::sketch::Point2d {
4403 x: crate::frontend::api::Expr::Var(unit_to_number(coords.x, default_unit, units)),
4404 y: crate::frontend::api::Expr::Var(unit_to_number(coords.y, default_unit, units)),
4405 };
4406
4407 let arc_ctor = SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4408 start: coords_to_point_expr(*arc_start_coords),
4409 end: coords_to_point_expr(*arc_end_coords),
4410 center: circle_ctor.center.clone(),
4411 construction: circle_ctor.construction,
4412 });
4413
4414 let (_add_source_delta, add_scene_graph_delta) = frontend
4415 .add_segment(ctx, version, sketch_id, arc_ctor, None)
4416 .await
4417 .map_err(|e| format!("Failed to add arc while replacing circle: {}", e.error.message()))?;
4418 invalidates_ids = invalidates_ids || add_scene_graph_delta.invalidates_ids;
4419
4420 let new_arc_id = *add_scene_graph_delta
4421 .new_objects
4422 .iter()
4423 .find(|&id| {
4424 add_scene_graph_delta
4425 .new_graph
4426 .objects
4427 .iter()
4428 .find(|o| o.id == *id)
4429 .is_some_and(|obj| {
4430 matches!(
4431 &obj.kind,
4432 crate::frontend::api::ObjectKind::Segment { segment }
4433 if matches!(segment, crate::frontend::sketch::Segment::Arc(_))
4434 )
4435 })
4436 })
4437 .ok_or_else(|| "Failed to find newly created arc segment".to_string())?;
4438
4439 let new_arc_obj = add_scene_graph_delta
4440 .new_graph
4441 .objects
4442 .iter()
4443 .find(|obj| obj.id == new_arc_id)
4444 .ok_or_else(|| format!("New arc segment not found {}", new_arc_id.0))?;
4445 let (new_arc_start_id, new_arc_end_id, new_arc_center_id) = match &new_arc_obj.kind {
4446 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4447 crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, arc.center),
4448 _ => return Err("New segment is not an arc".to_string()),
4449 },
4450 _ => return Err("New arc object is not a segment".to_string()),
4451 };
4452
4453 let constraint_segments_for =
4454 |arc_endpoint_id: ObjectId,
4455 term: &TrimTermination|
4456 -> Result<Vec<crate::frontend::sketch::ConstraintSegment>, String> {
4457 match term {
4458 TrimTermination::Intersection {
4459 intersecting_seg_id, ..
4460 } => Ok(vec![arc_endpoint_id.into(), (*intersecting_seg_id).into()]),
4461 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4462 other_segment_point_id,
4463 ..
4464 } => Ok(vec![arc_endpoint_id.into(), (*other_segment_point_id).into()]),
4465 TrimTermination::SegEndPoint { .. } => {
4466 Err("Circle replacement endpoint cannot terminate at seg endpoint".to_string())
4467 }
4468 }
4469 };
4470
4471 let start_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4472 segments: constraint_segments_for(new_arc_start_id, arc_start_termination)?,
4473 });
4474 let (_c1_source_delta, c1_scene_graph_delta) = frontend
4475 .add_constraint(ctx, version, sketch_id, start_constraint)
4476 .await
4477 .map_err(|e| format!("Failed to add start coincident on replaced arc: {}", e.error.message()))?;
4478 invalidates_ids = invalidates_ids || c1_scene_graph_delta.invalidates_ids;
4479
4480 let end_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4481 segments: constraint_segments_for(new_arc_end_id, arc_end_termination)?,
4482 });
4483 let (_c2_source_delta, c2_scene_graph_delta) = frontend
4484 .add_constraint(ctx, version, sketch_id, end_constraint)
4485 .await
4486 .map_err(|e| format!("Failed to add end coincident on replaced arc: {}", e.error.message()))?;
4487 invalidates_ids = invalidates_ids || c2_scene_graph_delta.invalidates_ids;
4488
4489 let mut termination_point_ids: Vec<ObjectId> = Vec::new();
4490 for term in [arc_start_termination, arc_end_termination] {
4491 if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4492 other_segment_point_id,
4493 ..
4494 } = term.as_ref()
4495 {
4496 termination_point_ids.push(*other_segment_point_id);
4497 }
4498 }
4499
4500 let rewrite_map = std::collections::HashMap::from([
4504 (*circle_id, new_arc_id),
4505 (original_circle_center_id, new_arc_center_id),
4506 (original_circle_start_id, new_arc_start_id),
4507 ]);
4508 let rewrite_ids: std::collections::HashSet<ObjectId> = rewrite_map.keys().copied().collect();
4509
4510 let mut migrated_constraints: Vec<Constraint> = Vec::new();
4511 for obj in ¤t_scene_graph_delta.new_graph.objects {
4512 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4513 continue;
4514 };
4515
4516 match constraint {
4517 Constraint::Coincident(coincident) => {
4518 if !constraint_segments_reference_any(&coincident.segments, &rewrite_ids) {
4519 continue;
4520 }
4521
4522 if coincident.contains_segment(*circle_id)
4526 && coincident
4527 .segment_ids()
4528 .filter(|id| *id != *circle_id)
4529 .any(|id| termination_point_ids.contains(&id))
4530 {
4531 continue;
4532 }
4533
4534 let Some(Constraint::Coincident(migrated_coincident)) =
4535 rewrite_constraint_with_map(constraint, &rewrite_map)
4536 else {
4537 continue;
4538 };
4539
4540 let migrated_ids: Vec<ObjectId> = migrated_coincident
4544 .segments
4545 .iter()
4546 .filter_map(|segment| match segment {
4547 crate::frontend::sketch::ConstraintSegment::Segment(id) => Some(*id),
4548 crate::frontend::sketch::ConstraintSegment::Origin(_) => None,
4549 })
4550 .collect();
4551 if migrated_ids.contains(&new_arc_id)
4552 && (migrated_ids.contains(&new_arc_start_id) || migrated_ids.contains(&new_arc_end_id))
4553 {
4554 continue;
4555 }
4556
4557 migrated_constraints.push(Constraint::Coincident(migrated_coincident));
4558 }
4559 Constraint::Distance(distance) => {
4560 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4561 continue;
4562 }
4563 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4564 migrated_constraints.push(migrated);
4565 }
4566 }
4567 Constraint::HorizontalDistance(distance) => {
4568 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4569 continue;
4570 }
4571 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4572 migrated_constraints.push(migrated);
4573 }
4574 }
4575 Constraint::VerticalDistance(distance) => {
4576 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4577 continue;
4578 }
4579 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4580 migrated_constraints.push(migrated);
4581 }
4582 }
4583 Constraint::Radius(radius) => {
4584 if radius.arc == *circle_id
4585 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4586 {
4587 migrated_constraints.push(migrated);
4588 }
4589 }
4590 Constraint::Diameter(diameter) => {
4591 if diameter.arc == *circle_id
4592 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4593 {
4594 migrated_constraints.push(migrated);
4595 }
4596 }
4597 Constraint::EqualRadius(equal_radius) => {
4598 if equal_radius.input.contains(circle_id)
4599 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4600 {
4601 migrated_constraints.push(migrated);
4602 }
4603 }
4604 Constraint::Tangent(tangent) => {
4605 if tangent.input.contains(circle_id)
4606 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4607 {
4608 migrated_constraints.push(migrated);
4609 }
4610 }
4611 _ => {}
4612 }
4613 }
4614
4615 for constraint in migrated_constraints {
4616 let (_source_delta, migrated_scene_graph_delta) = frontend
4617 .add_constraint(ctx, version, sketch_id, constraint)
4618 .await
4619 .map_err(|e| format!("Failed to migrate circle constraint to arc: {}", e.error.message()))?;
4620 invalidates_ids = invalidates_ids || migrated_scene_graph_delta.invalidates_ids;
4621 }
4622
4623 frontend
4624 .delete_objects(ctx, version, sketch_id, Vec::new(), vec![*circle_id])
4625 .await
4626 .map_err(|e| format!("Failed to delete circle after arc replacement: {}", e.error.message()))
4627 }
4628 TrimOperation::SplitSegment {
4629 segment_id,
4630 left_trim_coords,
4631 right_trim_coords,
4632 original_end_coords,
4633 left_side,
4634 right_side,
4635 constraints_to_migrate,
4636 constraints_to_delete,
4637 ..
4638 } => {
4639 let original_segment = current_scene_graph_delta
4644 .new_graph
4645 .objects
4646 .iter()
4647 .find(|obj| obj.id == *segment_id)
4648 .ok_or_else(|| format!("Failed to find original segment {}", segment_id.0))?;
4649
4650 let (original_segment_start_point_id, original_segment_end_point_id, original_segment_center_point_id) =
4652 match &original_segment.kind {
4653 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4654 crate::frontend::sketch::Segment::Line(line) => (Some(line.start), Some(line.end), None),
4655 crate::frontend::sketch::Segment::Arc(arc) => {
4656 (Some(arc.start), Some(arc.end), Some(arc.center))
4657 }
4658 _ => (None, None, None),
4659 },
4660 _ => (None, None, None),
4661 };
4662
4663 let mut center_point_constraints_to_migrate: Vec<(Constraint, ObjectId)> = Vec::new();
4665 if let Some(original_center_id) = original_segment_center_point_id {
4666 for obj in ¤t_scene_graph_delta.new_graph.objects {
4667 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4668 continue;
4669 };
4670
4671 if let Constraint::Coincident(coincident) = constraint
4673 && coincident.contains_segment(original_center_id)
4674 {
4675 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
4676 }
4677
4678 if let Constraint::Distance(distance) = constraint
4680 && distance.contains_point(original_center_id)
4681 {
4682 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
4683 }
4684 }
4685 }
4686
4687 let (_segment_type, original_ctor) = match &original_segment.kind {
4689 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4690 crate::frontend::sketch::Segment::Line(line) => ("Line", line.ctor.clone()),
4691 crate::frontend::sketch::Segment::Arc(arc) => ("Arc", arc.ctor.clone()),
4692 _ => {
4693 return Err("Original segment is not a Line or Arc".to_string());
4694 }
4695 },
4696 _ => {
4697 return Err("Original object is not a segment".to_string());
4698 }
4699 };
4700
4701 let units = match &original_ctor {
4703 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
4704 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4705 _ => crate::pretty::NumericSuffix::Mm,
4706 },
4707 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
4708 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4709 _ => crate::pretty::NumericSuffix::Mm,
4710 },
4711 _ => crate::pretty::NumericSuffix::Mm,
4712 };
4713
4714 let coords_to_point =
4717 |coords: Coords2d| -> crate::frontend::sketch::Point2d<crate::frontend::api::Number> {
4718 crate::frontend::sketch::Point2d {
4719 x: unit_to_number(coords.x, default_unit, units),
4720 y: unit_to_number(coords.y, default_unit, units),
4721 }
4722 };
4723
4724 let point_to_expr = |point: crate::frontend::sketch::Point2d<crate::frontend::api::Number>| -> crate::frontend::sketch::Point2d<crate::frontend::api::Expr> {
4726 crate::frontend::sketch::Point2d {
4727 x: crate::frontend::api::Expr::Var(point.x),
4728 y: crate::frontend::api::Expr::Var(point.y),
4729 }
4730 };
4731
4732 let new_segment_ctor = match &original_ctor {
4734 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
4735 start: point_to_expr(coords_to_point(*right_trim_coords)),
4736 end: point_to_expr(coords_to_point(*original_end_coords)),
4737 construction: line_ctor.construction,
4738 }),
4739 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4740 start: point_to_expr(coords_to_point(*right_trim_coords)),
4741 end: point_to_expr(coords_to_point(*original_end_coords)),
4742 center: arc_ctor.center.clone(),
4743 construction: arc_ctor.construction,
4744 }),
4745 _ => {
4746 return Err("Unsupported segment type for new segment".to_string());
4747 }
4748 };
4749
4750 let (_add_source_delta, add_scene_graph_delta) = frontend
4751 .add_segment(ctx, version, sketch_id, new_segment_ctor, None)
4752 .await
4753 .map_err(|e| format!("Failed to add new segment: {}", e.error.message()))?;
4754
4755 let new_segment_id = *add_scene_graph_delta
4757 .new_objects
4758 .iter()
4759 .find(|&id| {
4760 if let Some(obj) = add_scene_graph_delta.new_graph.objects.iter().find(|o| o.id == *id) {
4761 matches!(
4762 &obj.kind,
4763 crate::frontend::api::ObjectKind::Segment { segment }
4764 if matches!(segment, crate::frontend::sketch::Segment::Line(_) | crate::frontend::sketch::Segment::Arc(_))
4765 )
4766 } else {
4767 false
4768 }
4769 })
4770 .ok_or_else(|| "Failed to find newly created segment".to_string())?;
4771
4772 let new_segment = add_scene_graph_delta
4773 .new_graph
4774 .objects
4775 .iter()
4776 .find(|o| o.id == new_segment_id)
4777 .ok_or_else(|| format!("New segment not found with id {}", new_segment_id.0))?;
4778
4779 let (new_segment_start_point_id, new_segment_end_point_id, new_segment_center_point_id) =
4781 match &new_segment.kind {
4782 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4783 crate::frontend::sketch::Segment::Line(line) => (line.start, line.end, None),
4784 crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, Some(arc.center)),
4785 _ => {
4786 return Err("New segment is not a Line or Arc".to_string());
4787 }
4788 },
4789 _ => {
4790 return Err("New segment is not a segment".to_string());
4791 }
4792 };
4793
4794 let edited_ctor = match &original_ctor {
4796 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
4797 start: line_ctor.start.clone(),
4798 end: point_to_expr(coords_to_point(*left_trim_coords)),
4799 construction: line_ctor.construction,
4800 }),
4801 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4802 start: arc_ctor.start.clone(),
4803 end: point_to_expr(coords_to_point(*left_trim_coords)),
4804 center: arc_ctor.center.clone(),
4805 construction: arc_ctor.construction,
4806 }),
4807 _ => {
4808 return Err("Unsupported segment type for split".to_string());
4809 }
4810 };
4811
4812 let (_edit_source_delta, edit_scene_graph_delta) = frontend
4813 .edit_segments(
4814 ctx,
4815 version,
4816 sketch_id,
4817 vec![ExistingSegmentCtor {
4818 id: *segment_id,
4819 ctor: edited_ctor,
4820 }],
4821 )
4822 .await
4823 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))?;
4824 invalidates_ids = invalidates_ids || edit_scene_graph_delta.invalidates_ids;
4826
4827 let edited_segment = edit_scene_graph_delta
4829 .new_graph
4830 .objects
4831 .iter()
4832 .find(|obj| obj.id == *segment_id)
4833 .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
4834
4835 let left_side_endpoint_point_id = match &edited_segment.kind {
4836 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4837 crate::frontend::sketch::Segment::Line(line) => line.end,
4838 crate::frontend::sketch::Segment::Arc(arc) => arc.end,
4839 _ => {
4840 return Err("Edited segment is not a Line or Arc".to_string());
4841 }
4842 },
4843 _ => {
4844 return Err("Edited segment is not a segment".to_string());
4845 }
4846 };
4847
4848 let mut batch_constraints = Vec::new();
4850
4851 let left_intersecting_seg_id = match &**left_side {
4853 TrimTermination::Intersection {
4854 intersecting_seg_id, ..
4855 }
4856 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4857 intersecting_seg_id, ..
4858 } => *intersecting_seg_id,
4859 _ => {
4860 return Err("Left side is not an intersection or coincident".to_string());
4861 }
4862 };
4863 let left_coincident_segments = match &**left_side {
4864 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4865 other_segment_point_id,
4866 ..
4867 } => {
4868 vec![left_side_endpoint_point_id.into(), (*other_segment_point_id).into()]
4869 }
4870 _ => {
4871 vec![left_side_endpoint_point_id.into(), left_intersecting_seg_id.into()]
4872 }
4873 };
4874 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
4875 segments: left_coincident_segments,
4876 }));
4877
4878 let right_intersecting_seg_id = match &**right_side {
4880 TrimTermination::Intersection {
4881 intersecting_seg_id, ..
4882 }
4883 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4884 intersecting_seg_id, ..
4885 } => *intersecting_seg_id,
4886 _ => {
4887 return Err("Right side is not an intersection or coincident".to_string());
4888 }
4889 };
4890
4891 let mut intersection_point_id: Option<ObjectId> = None;
4892 if matches!(&**right_side, TrimTermination::Intersection { .. }) {
4893 let intersecting_seg = edit_scene_graph_delta
4894 .new_graph
4895 .objects
4896 .iter()
4897 .find(|obj| obj.id == right_intersecting_seg_id);
4898
4899 if let Some(seg) = intersecting_seg {
4900 let endpoint_epsilon = 1e-3; let right_trim_coords_value = *right_trim_coords;
4902
4903 if let crate::frontend::api::ObjectKind::Segment { segment } = &seg.kind {
4904 match segment {
4905 crate::frontend::sketch::Segment::Line(_) => {
4906 if let (Some(start_coords), Some(end_coords)) = (
4907 crate::frontend::trim::get_position_coords_for_line(
4908 seg,
4909 crate::frontend::trim::LineEndpoint::Start,
4910 &edit_scene_graph_delta.new_graph.objects,
4911 default_unit,
4912 ),
4913 crate::frontend::trim::get_position_coords_for_line(
4914 seg,
4915 crate::frontend::trim::LineEndpoint::End,
4916 &edit_scene_graph_delta.new_graph.objects,
4917 default_unit,
4918 ),
4919 ) {
4920 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
4921 * (right_trim_coords_value.x - start_coords.x)
4922 + (right_trim_coords_value.y - start_coords.y)
4923 * (right_trim_coords_value.y - start_coords.y))
4924 .sqrt();
4925 if dist_to_start < endpoint_epsilon {
4926 if let crate::frontend::sketch::Segment::Line(line) = segment {
4927 intersection_point_id = Some(line.start);
4928 }
4929 } else {
4930 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
4931 * (right_trim_coords_value.x - end_coords.x)
4932 + (right_trim_coords_value.y - end_coords.y)
4933 * (right_trim_coords_value.y - end_coords.y))
4934 .sqrt();
4935 if dist_to_end < endpoint_epsilon
4936 && let crate::frontend::sketch::Segment::Line(line) = segment
4937 {
4938 intersection_point_id = Some(line.end);
4939 }
4940 }
4941 }
4942 }
4943 crate::frontend::sketch::Segment::Arc(_) => {
4944 if let (Some(start_coords), Some(end_coords)) = (
4945 crate::frontend::trim::get_position_coords_from_arc(
4946 seg,
4947 crate::frontend::trim::ArcPoint::Start,
4948 &edit_scene_graph_delta.new_graph.objects,
4949 default_unit,
4950 ),
4951 crate::frontend::trim::get_position_coords_from_arc(
4952 seg,
4953 crate::frontend::trim::ArcPoint::End,
4954 &edit_scene_graph_delta.new_graph.objects,
4955 default_unit,
4956 ),
4957 ) {
4958 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
4959 * (right_trim_coords_value.x - start_coords.x)
4960 + (right_trim_coords_value.y - start_coords.y)
4961 * (right_trim_coords_value.y - start_coords.y))
4962 .sqrt();
4963 if dist_to_start < endpoint_epsilon {
4964 if let crate::frontend::sketch::Segment::Arc(arc) = segment {
4965 intersection_point_id = Some(arc.start);
4966 }
4967 } else {
4968 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
4969 * (right_trim_coords_value.x - end_coords.x)
4970 + (right_trim_coords_value.y - end_coords.y)
4971 * (right_trim_coords_value.y - end_coords.y))
4972 .sqrt();
4973 if dist_to_end < endpoint_epsilon
4974 && let crate::frontend::sketch::Segment::Arc(arc) = segment
4975 {
4976 intersection_point_id = Some(arc.end);
4977 }
4978 }
4979 }
4980 }
4981 _ => {}
4982 }
4983 }
4984 }
4985 }
4986
4987 let right_coincident_segments = if let Some(point_id) = intersection_point_id {
4988 vec![new_segment_start_point_id.into(), point_id.into()]
4989 } else if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4990 other_segment_point_id,
4991 ..
4992 } = &**right_side
4993 {
4994 vec![new_segment_start_point_id.into(), (*other_segment_point_id).into()]
4995 } else {
4996 vec![new_segment_start_point_id.into(), right_intersecting_seg_id.into()]
4997 };
4998 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
4999 segments: right_coincident_segments,
5000 }));
5001
5002 let mut points_constrained_to_new_segment_start = std::collections::HashSet::new();
5004 let mut points_constrained_to_new_segment_end = std::collections::HashSet::new();
5005
5006 if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5007 other_segment_point_id,
5008 ..
5009 } = &**right_side
5010 {
5011 points_constrained_to_new_segment_start.insert(other_segment_point_id);
5012 }
5013
5014 for constraint_to_migrate in constraints_to_migrate.iter() {
5015 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::End
5016 && constraint_to_migrate.is_point_point
5017 {
5018 points_constrained_to_new_segment_end.insert(constraint_to_migrate.other_entity_id);
5019 }
5020 }
5021
5022 for constraint_to_migrate in constraints_to_migrate.iter() {
5023 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment
5025 && (points_constrained_to_new_segment_start.contains(&constraint_to_migrate.other_entity_id)
5026 || points_constrained_to_new_segment_end.contains(&constraint_to_migrate.other_entity_id))
5027 {
5028 continue; }
5030
5031 let constraint_segments = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment {
5032 vec![constraint_to_migrate.other_entity_id.into(), new_segment_id.into()]
5033 } else {
5034 let target_endpoint_id = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Start
5035 {
5036 new_segment_start_point_id
5037 } else {
5038 new_segment_end_point_id
5039 };
5040 vec![target_endpoint_id.into(), constraint_to_migrate.other_entity_id.into()]
5041 };
5042 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
5043 segments: constraint_segments,
5044 }));
5045 }
5046
5047 let mut distance_constraints_to_re_add: Vec<(
5049 crate::frontend::api::Number,
5050 crate::frontend::sketch::ConstraintSource,
5051 )> = Vec::new();
5052 if let (Some(original_start_id), Some(original_end_id)) =
5053 (original_segment_start_point_id, original_segment_end_point_id)
5054 {
5055 for obj in &edit_scene_graph_delta.new_graph.objects {
5056 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5057 continue;
5058 };
5059
5060 let Constraint::Distance(distance) = constraint else {
5061 continue;
5062 };
5063
5064 let references_start = distance.contains_point(original_start_id);
5065 let references_end = distance.contains_point(original_end_id);
5066
5067 if references_start && references_end {
5068 distance_constraints_to_re_add.push((distance.distance, distance.source.clone()));
5069 }
5070 }
5071 }
5072
5073 if let Some(original_start_id) = original_segment_start_point_id {
5075 for (distance_value, source) in distance_constraints_to_re_add {
5076 batch_constraints.push(Constraint::Distance(crate::frontend::sketch::Distance {
5077 points: vec![original_start_id.into(), new_segment_end_point_id.into()],
5078 distance: distance_value,
5079 source,
5080 }));
5081 }
5082 }
5083
5084 if let Some(new_center_id) = new_segment_center_point_id {
5086 for (constraint, original_center_id) in center_point_constraints_to_migrate {
5087 let center_rewrite_map = std::collections::HashMap::from([(original_center_id, new_center_id)]);
5088 if let Some(rewritten) = rewrite_constraint_with_map(&constraint, ¢er_rewrite_map)
5089 && matches!(rewritten, Constraint::Coincident(_) | Constraint::Distance(_))
5090 {
5091 batch_constraints.push(rewritten);
5092 }
5093 }
5094 }
5095
5096 let angle_rewrite_map = std::collections::HashMap::from([(*segment_id, new_segment_id)]);
5098 for obj in &edit_scene_graph_delta.new_graph.objects {
5099 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5100 continue;
5101 };
5102
5103 let should_migrate = match constraint {
5104 Constraint::Parallel(parallel) => parallel.lines.contains(segment_id),
5105 Constraint::Perpendicular(perpendicular) => perpendicular.lines.contains(segment_id),
5106 Constraint::Horizontal(horizontal) => horizontal.line == *segment_id,
5107 Constraint::Vertical(vertical) => vertical.line == *segment_id,
5108 _ => false,
5109 };
5110
5111 if should_migrate
5112 && let Some(migrated_constraint) = rewrite_constraint_with_map(constraint, &angle_rewrite_map)
5113 && matches!(
5114 migrated_constraint,
5115 Constraint::Parallel(_)
5116 | Constraint::Perpendicular(_)
5117 | Constraint::Horizontal(_)
5118 | Constraint::Vertical(_)
5119 )
5120 {
5121 batch_constraints.push(migrated_constraint);
5122 }
5123 }
5124
5125 let constraint_object_ids: Vec<ObjectId> = constraints_to_delete.to_vec();
5127
5128 let batch_result = frontend
5129 .batch_split_segment_operations(
5130 ctx,
5131 version,
5132 sketch_id,
5133 Vec::new(), batch_constraints,
5135 constraint_object_ids,
5136 crate::frontend::sketch::NewSegmentInfo {
5137 segment_id: new_segment_id,
5138 start_point_id: new_segment_start_point_id,
5139 end_point_id: new_segment_end_point_id,
5140 center_point_id: new_segment_center_point_id,
5141 },
5142 )
5143 .await
5144 .map_err(|e| format!("Failed to batch split segment operations: {}", e.error.message()));
5145 if let Ok((_, ref batch_delta)) = batch_result {
5147 invalidates_ids = invalidates_ids || batch_delta.invalidates_ids;
5148 }
5149 batch_result
5150 }
5151 };
5152
5153 match operation_result {
5154 Ok((source_delta, scene_graph_delta)) => {
5155 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
5157 last_result = Some((source_delta, scene_graph_delta.clone()));
5158 }
5159 Err(e) => {
5160 crate::logln!("Error executing trim operation {}: {}", op_index, e);
5161 }
5163 }
5164
5165 op_index += consumed_ops;
5166 }
5167
5168 let (source_delta, mut scene_graph_delta) =
5169 last_result.ok_or_else(|| "No operations were executed successfully".to_string())?;
5170 scene_graph_delta.invalidates_ids = invalidates_ids;
5172 Ok((source_delta, scene_graph_delta))
5173}