1use std::f64::consts::TAU;
2
3use indexmap::IndexSet;
4use kittycad_modeling_cmds::units::UnitLength;
5
6use crate::execution::types::adjust_length;
7use crate::front::Horizontal;
8use crate::front::Vertical;
9use crate::frontend::api::Number;
10use crate::frontend::api::Object;
11use crate::frontend::api::ObjectId;
12use crate::frontend::api::ObjectKind;
13use crate::frontend::sketch::Constraint;
14use crate::frontend::sketch::ConstraintSegment;
15use crate::frontend::sketch::Segment;
16use crate::frontend::sketch::SegmentCtor;
17use crate::pretty::NumericSuffix;
18
19#[cfg(all(feature = "artifact-graph", test))]
20mod tests;
21
22const EPSILON_PARALLEL: f64 = 1e-10;
24const EPSILON_POINT_ON_SEGMENT: f64 = 1e-6;
25
26fn suffix_to_unit(suffix: NumericSuffix) -> UnitLength {
28 match suffix {
29 NumericSuffix::Mm => UnitLength::Millimeters,
30 NumericSuffix::Cm => UnitLength::Centimeters,
31 NumericSuffix::M => UnitLength::Meters,
32 NumericSuffix::Inch => UnitLength::Inches,
33 NumericSuffix::Ft => UnitLength::Feet,
34 NumericSuffix::Yd => UnitLength::Yards,
35 _ => UnitLength::Millimeters,
36 }
37}
38
39fn number_to_unit(n: &Number, target_unit: UnitLength) -> f64 {
41 adjust_length(suffix_to_unit(n.units), n.value, target_unit).0
42}
43
44fn unit_to_number(value: f64, source_unit: UnitLength, target_suffix: NumericSuffix) -> Number {
46 let (value, _) = adjust_length(source_unit, value, suffix_to_unit(target_suffix));
47 Number {
48 value,
49 units: target_suffix,
50 }
51}
52
53fn normalize_trim_points_to_unit(points: &[Coords2d], default_unit: UnitLength) -> Vec<Coords2d> {
55 points
56 .iter()
57 .map(|point| Coords2d {
58 x: adjust_length(UnitLength::Millimeters, point.x, default_unit).0,
59 y: adjust_length(UnitLength::Millimeters, point.y, default_unit).0,
60 })
61 .collect()
62}
63
64#[derive(Debug, Clone, Copy)]
66pub struct Coords2d {
67 pub x: f64,
68 pub y: f64,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum LineEndpoint {
74 Start,
75 End,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum ArcPoint {
81 Start,
82 End,
83 Center,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum CirclePoint {
89 Start,
90 Center,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum TrimDirection {
96 Left,
97 Right,
98}
99
100#[derive(Debug, Clone)]
108pub enum TrimItem {
109 Spawn {
110 trim_spawn_seg_id: ObjectId,
111 trim_spawn_coords: Coords2d,
112 next_index: usize,
113 },
114 None {
115 next_index: usize,
116 },
117}
118
119#[derive(Debug, Clone)]
126pub enum TrimTermination {
127 SegEndPoint {
128 trim_termination_coords: Coords2d,
129 },
130 Intersection {
131 trim_termination_coords: Coords2d,
132 intersecting_seg_id: ObjectId,
133 },
134 TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
135 trim_termination_coords: Coords2d,
136 intersecting_seg_id: ObjectId,
137 other_segment_point_id: ObjectId,
138 },
139}
140
141#[derive(Debug, Clone)]
143pub struct TrimTerminations {
144 pub left_side: TrimTermination,
145 pub right_side: TrimTermination,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum AttachToEndpoint {
151 Start,
152 End,
153 Segment,
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum EndpointChanged {
159 Start,
160 End,
161}
162
163#[derive(Debug, Clone)]
165pub struct CoincidentData {
166 pub intersecting_seg_id: ObjectId,
167 pub intersecting_endpoint_point_id: Option<ObjectId>,
168 pub existing_point_segment_constraint_id: Option<ObjectId>,
169}
170
171#[derive(Debug, Clone)]
173pub struct ConstraintToMigrate {
174 pub constraint_id: ObjectId,
175 pub other_entity_id: ObjectId,
176 pub is_point_point: bool,
179 pub attach_to_endpoint: AttachToEndpoint,
180}
181
182#[derive(Debug, Clone)]
184pub enum TrimPlan {
185 DeleteSegment {
186 segment_id: ObjectId,
187 },
188 TailCut {
189 segment_id: ObjectId,
190 endpoint_changed: EndpointChanged,
191 ctor: SegmentCtor,
192 segment_or_point_to_make_coincident_to: ObjectId,
193 intersecting_endpoint_point_id: Option<ObjectId>,
194 constraint_ids_to_delete: Vec<ObjectId>,
195 },
196 ReplaceCircleWithArc {
197 circle_id: ObjectId,
198 arc_start_coords: Coords2d,
199 arc_end_coords: Coords2d,
200 arc_start_termination: Box<TrimTermination>,
201 arc_end_termination: Box<TrimTermination>,
202 },
203 SplitSegment {
204 segment_id: ObjectId,
205 left_trim_coords: Coords2d,
206 right_trim_coords: Coords2d,
207 original_end_coords: Coords2d,
208 left_side: Box<TrimTermination>,
209 right_side: Box<TrimTermination>,
210 left_side_coincident_data: CoincidentData,
211 right_side_coincident_data: CoincidentData,
212 constraints_to_migrate: Vec<ConstraintToMigrate>,
213 constraints_to_delete: Vec<ObjectId>,
214 },
215}
216
217fn lower_trim_plan(plan: &TrimPlan) -> Vec<TrimOperation> {
218 match plan {
219 TrimPlan::DeleteSegment { segment_id } => vec![TrimOperation::SimpleTrim {
220 segment_to_trim_id: *segment_id,
221 }],
222 TrimPlan::TailCut {
223 segment_id,
224 endpoint_changed,
225 ctor,
226 segment_or_point_to_make_coincident_to,
227 intersecting_endpoint_point_id,
228 constraint_ids_to_delete,
229 } => {
230 let mut ops = vec![
231 TrimOperation::EditSegment {
232 segment_id: *segment_id,
233 ctor: ctor.clone(),
234 endpoint_changed: *endpoint_changed,
235 },
236 TrimOperation::AddCoincidentConstraint {
237 segment_id: *segment_id,
238 endpoint_changed: *endpoint_changed,
239 segment_or_point_to_make_coincident_to: *segment_or_point_to_make_coincident_to,
240 intersecting_endpoint_point_id: *intersecting_endpoint_point_id,
241 },
242 ];
243 if !constraint_ids_to_delete.is_empty() {
244 ops.push(TrimOperation::DeleteConstraints {
245 constraint_ids: constraint_ids_to_delete.clone(),
246 });
247 }
248 ops
249 }
250 TrimPlan::ReplaceCircleWithArc {
251 circle_id,
252 arc_start_coords,
253 arc_end_coords,
254 arc_start_termination,
255 arc_end_termination,
256 } => vec![TrimOperation::ReplaceCircleWithArc {
257 circle_id: *circle_id,
258 arc_start_coords: *arc_start_coords,
259 arc_end_coords: *arc_end_coords,
260 arc_start_termination: arc_start_termination.clone(),
261 arc_end_termination: arc_end_termination.clone(),
262 }],
263 TrimPlan::SplitSegment {
264 segment_id,
265 left_trim_coords,
266 right_trim_coords,
267 original_end_coords,
268 left_side,
269 right_side,
270 left_side_coincident_data,
271 right_side_coincident_data,
272 constraints_to_migrate,
273 constraints_to_delete,
274 } => vec![TrimOperation::SplitSegment {
275 segment_id: *segment_id,
276 left_trim_coords: *left_trim_coords,
277 right_trim_coords: *right_trim_coords,
278 original_end_coords: *original_end_coords,
279 left_side: left_side.clone(),
280 right_side: right_side.clone(),
281 left_side_coincident_data: left_side_coincident_data.clone(),
282 right_side_coincident_data: right_side_coincident_data.clone(),
283 constraints_to_migrate: constraints_to_migrate.clone(),
284 constraints_to_delete: constraints_to_delete.clone(),
285 }],
286 }
287}
288
289fn trim_plan_modifies_geometry(plan: &TrimPlan) -> bool {
290 matches!(
291 plan,
292 TrimPlan::DeleteSegment { .. }
293 | TrimPlan::TailCut { .. }
294 | TrimPlan::ReplaceCircleWithArc { .. }
295 | TrimPlan::SplitSegment { .. }
296 )
297}
298
299fn rewrite_object_id(id: ObjectId, rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>) -> ObjectId {
300 rewrite_map.get(&id).copied().unwrap_or(id)
301}
302
303fn rewrite_constraint_segment(
304 segment: crate::frontend::sketch::ConstraintSegment,
305 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
306) -> crate::frontend::sketch::ConstraintSegment {
307 match segment {
308 crate::frontend::sketch::ConstraintSegment::Segment(id) => {
309 crate::frontend::sketch::ConstraintSegment::Segment(rewrite_object_id(id, rewrite_map))
310 }
311 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
312 crate::frontend::sketch::ConstraintSegment::Origin(origin)
313 }
314 }
315}
316
317fn rewrite_constraint_segments(
318 segments: &[crate::frontend::sketch::ConstraintSegment],
319 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
320) -> Vec<crate::frontend::sketch::ConstraintSegment> {
321 segments
322 .iter()
323 .copied()
324 .map(|segment| rewrite_constraint_segment(segment, rewrite_map))
325 .collect()
326}
327
328fn constraint_segments_reference_any(
329 segments: &[crate::frontend::sketch::ConstraintSegment],
330 ids: &std::collections::HashSet<ObjectId>,
331) -> bool {
332 segments.iter().any(|segment| match segment {
333 crate::frontend::sketch::ConstraintSegment::Segment(id) => ids.contains(id),
334 crate::frontend::sketch::ConstraintSegment::Origin(_) => false,
335 })
336}
337
338fn rewrite_constraint_with_map(
339 constraint: &Constraint,
340 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
341) -> Option<Constraint> {
342 match constraint {
343 Constraint::Coincident(coincident) => Some(Constraint::Coincident(crate::frontend::sketch::Coincident {
344 segments: rewrite_constraint_segments(&coincident.segments, rewrite_map),
345 })),
346 Constraint::Distance(distance) => Some(Constraint::Distance(crate::frontend::sketch::Distance {
347 points: rewrite_constraint_segments(&distance.points, rewrite_map),
348 distance: distance.distance,
349 source: distance.source.clone(),
350 })),
351 Constraint::HorizontalDistance(distance) => {
352 Some(Constraint::HorizontalDistance(crate::frontend::sketch::Distance {
353 points: rewrite_constraint_segments(&distance.points, rewrite_map),
354 distance: distance.distance,
355 source: distance.source.clone(),
356 }))
357 }
358 Constraint::VerticalDistance(distance) => {
359 Some(Constraint::VerticalDistance(crate::frontend::sketch::Distance {
360 points: rewrite_constraint_segments(&distance.points, rewrite_map),
361 distance: distance.distance,
362 source: distance.source.clone(),
363 }))
364 }
365 Constraint::Radius(radius) => Some(Constraint::Radius(crate::frontend::sketch::Radius {
366 arc: rewrite_object_id(radius.arc, rewrite_map),
367 radius: radius.radius,
368 source: radius.source.clone(),
369 })),
370 Constraint::Diameter(diameter) => Some(Constraint::Diameter(crate::frontend::sketch::Diameter {
371 arc: rewrite_object_id(diameter.arc, rewrite_map),
372 diameter: diameter.diameter,
373 source: diameter.source.clone(),
374 })),
375 Constraint::EqualRadius(equal_radius) => Some(Constraint::EqualRadius(crate::frontend::sketch::EqualRadius {
376 input: equal_radius
377 .input
378 .iter()
379 .map(|id| rewrite_object_id(*id, rewrite_map))
380 .collect(),
381 })),
382 Constraint::Midpoint(midpoint) => Some(Constraint::Midpoint(crate::frontend::sketch::Midpoint {
383 point: rewrite_object_id(midpoint.point, rewrite_map),
384 segment: rewrite_object_id(midpoint.segment, rewrite_map),
385 })),
386 Constraint::Tangent(tangent) => Some(Constraint::Tangent(crate::frontend::sketch::Tangent {
387 input: tangent
388 .input
389 .iter()
390 .map(|id| rewrite_object_id(*id, rewrite_map))
391 .collect(),
392 })),
393 Constraint::Symmetric(symmetric) => Some(Constraint::Symmetric(crate::frontend::sketch::Symmetric {
394 input: symmetric
395 .input
396 .iter()
397 .map(|id| rewrite_object_id(*id, rewrite_map))
398 .collect(),
399 axis: rewrite_object_id(symmetric.axis, rewrite_map),
400 })),
401 Constraint::Parallel(parallel) => Some(Constraint::Parallel(crate::frontend::sketch::Parallel {
402 lines: parallel
403 .lines
404 .iter()
405 .map(|id| rewrite_object_id(*id, rewrite_map))
406 .collect(),
407 })),
408 Constraint::Perpendicular(perpendicular) => {
409 Some(Constraint::Perpendicular(crate::frontend::sketch::Perpendicular {
410 lines: perpendicular
411 .lines
412 .iter()
413 .map(|id| rewrite_object_id(*id, rewrite_map))
414 .collect(),
415 }))
416 }
417 Constraint::Horizontal(horizontal) => match horizontal {
418 crate::front::Horizontal::Line { line } => {
419 Some(Constraint::Horizontal(crate::frontend::sketch::Horizontal::Line {
420 line: rewrite_object_id(*line, rewrite_map),
421 }))
422 }
423 crate::front::Horizontal::Points { points } => Some(Constraint::Horizontal(Horizontal::Points {
424 points: points
425 .iter()
426 .map(|point| match point {
427 crate::frontend::sketch::ConstraintSegment::Segment(point) => {
428 crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
429 }
430 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
431 crate::frontend::sketch::ConstraintSegment::Origin(*origin)
432 }
433 })
434 .collect(),
435 })),
436 },
437 Constraint::Vertical(vertical) => match vertical {
438 crate::front::Vertical::Line { line } => {
439 Some(Constraint::Vertical(crate::frontend::sketch::Vertical::Line {
440 line: rewrite_object_id(*line, rewrite_map),
441 }))
442 }
443 crate::front::Vertical::Points { points } => Some(Constraint::Vertical(Vertical::Points {
444 points: points
445 .iter()
446 .map(|point| match point {
447 crate::frontend::sketch::ConstraintSegment::Segment(point) => {
448 crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
449 }
450 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
451 crate::frontend::sketch::ConstraintSegment::Origin(*origin)
452 }
453 })
454 .collect(),
455 })),
456 },
457 _ => None,
458 }
459}
460
461fn point_axis_constraint_references_point(constraint: &Constraint, point_id: ObjectId) -> bool {
462 match constraint {
463 Constraint::Horizontal(Horizontal::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
464 Constraint::Vertical(Vertical::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
465 _ => false,
466 }
467}
468
469#[derive(Debug, Clone)]
470#[allow(clippy::large_enum_variant)]
471pub enum TrimOperation {
472 SimpleTrim {
473 segment_to_trim_id: ObjectId,
474 },
475 EditSegment {
476 segment_id: ObjectId,
477 ctor: SegmentCtor,
478 endpoint_changed: EndpointChanged,
479 },
480 AddCoincidentConstraint {
481 segment_id: ObjectId,
482 endpoint_changed: EndpointChanged,
483 segment_or_point_to_make_coincident_to: ObjectId,
484 intersecting_endpoint_point_id: Option<ObjectId>,
485 },
486 SplitSegment {
487 segment_id: ObjectId,
488 left_trim_coords: Coords2d,
489 right_trim_coords: Coords2d,
490 original_end_coords: Coords2d,
491 left_side: Box<TrimTermination>,
492 right_side: Box<TrimTermination>,
493 left_side_coincident_data: CoincidentData,
494 right_side_coincident_data: CoincidentData,
495 constraints_to_migrate: Vec<ConstraintToMigrate>,
496 constraints_to_delete: Vec<ObjectId>,
497 },
498 ReplaceCircleWithArc {
499 circle_id: ObjectId,
500 arc_start_coords: Coords2d,
501 arc_end_coords: Coords2d,
502 arc_start_termination: Box<TrimTermination>,
503 arc_end_termination: Box<TrimTermination>,
504 },
505 DeleteConstraints {
506 constraint_ids: Vec<ObjectId>,
507 },
508}
509
510pub fn is_point_on_line_segment(
514 point: Coords2d,
515 segment_start: Coords2d,
516 segment_end: Coords2d,
517 epsilon: f64,
518) -> Option<Coords2d> {
519 let dx = segment_end.x - segment_start.x;
520 let dy = segment_end.y - segment_start.y;
521 let segment_length_sq = dx * dx + dy * dy;
522
523 if segment_length_sq < EPSILON_PARALLEL {
524 let dist_sq = (point.x - segment_start.x) * (point.x - segment_start.x)
526 + (point.y - segment_start.y) * (point.y - segment_start.y);
527 if dist_sq <= epsilon * epsilon {
528 return Some(point);
529 }
530 return None;
531 }
532
533 let point_dx = point.x - segment_start.x;
534 let point_dy = point.y - segment_start.y;
535 let projection_param = (point_dx * dx + point_dy * dy) / segment_length_sq;
536
537 if !(0.0..=1.0).contains(&projection_param) {
539 return None;
540 }
541
542 let projected_point = Coords2d {
544 x: segment_start.x + projection_param * dx,
545 y: segment_start.y + projection_param * dy,
546 };
547
548 let dist_dx = point.x - projected_point.x;
550 let dist_dy = point.y - projected_point.y;
551 let distance_sq = dist_dx * dist_dx + dist_dy * dist_dy;
552
553 if distance_sq <= epsilon * epsilon {
554 Some(point)
555 } else {
556 None
557 }
558}
559
560pub fn line_segment_intersection(
564 line1_start: Coords2d,
565 line1_end: Coords2d,
566 line2_start: Coords2d,
567 line2_end: Coords2d,
568 epsilon: f64,
569) -> Option<Coords2d> {
570 if let Some(point) = is_point_on_line_segment(line1_start, line2_start, line2_end, epsilon) {
572 return Some(point);
573 }
574
575 if let Some(point) = is_point_on_line_segment(line1_end, line2_start, line2_end, epsilon) {
576 return Some(point);
577 }
578
579 if let Some(point) = is_point_on_line_segment(line2_start, line1_start, line1_end, epsilon) {
580 return Some(point);
581 }
582
583 if let Some(point) = is_point_on_line_segment(line2_end, line1_start, line1_end, epsilon) {
584 return Some(point);
585 }
586
587 let x1 = line1_start.x;
589 let y1 = line1_start.y;
590 let x2 = line1_end.x;
591 let y2 = line1_end.y;
592 let x3 = line2_start.x;
593 let y3 = line2_start.y;
594 let x4 = line2_end.x;
595 let y4 = line2_end.y;
596
597 let denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
598 if denominator.abs() < EPSILON_PARALLEL {
599 return None;
601 }
602
603 let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denominator;
604 let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denominator;
605
606 if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
608 let x = x1 + t * (x2 - x1);
609 let y = y1 + t * (y2 - y1);
610 return Some(Coords2d { x, y });
611 }
612
613 None
614}
615
616pub fn project_point_onto_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
621 let dx = segment_end.x - segment_start.x;
622 let dy = segment_end.y - segment_start.y;
623 let segment_length_sq = dx * dx + dy * dy;
624
625 if segment_length_sq < EPSILON_PARALLEL {
626 return 0.0;
628 }
629
630 let point_dx = point.x - segment_start.x;
631 let point_dy = point.y - segment_start.y;
632
633 (point_dx * dx + point_dy * dy) / segment_length_sq
634}
635
636pub fn perpendicular_distance_to_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
640 let dx = segment_end.x - segment_start.x;
641 let dy = segment_end.y - segment_start.y;
642 let segment_length_sq = dx * dx + dy * dy;
643
644 if segment_length_sq < EPSILON_PARALLEL {
645 let dist_dx = point.x - segment_start.x;
647 let dist_dy = point.y - segment_start.y;
648 return (dist_dx * dist_dx + dist_dy * dist_dy).sqrt();
649 }
650
651 let point_dx = point.x - segment_start.x;
653 let point_dy = point.y - segment_start.y;
654
655 let t = (point_dx * dx + point_dy * dy) / segment_length_sq;
657
658 let clamped_t = t.clamp(0.0, 1.0);
660 let closest_point = Coords2d {
661 x: segment_start.x + clamped_t * dx,
662 y: segment_start.y + clamped_t * dy,
663 };
664
665 let dist_dx = point.x - closest_point.x;
667 let dist_dy = point.y - closest_point.y;
668 (dist_dx * dist_dx + dist_dy * dist_dy).sqrt()
669}
670
671pub fn is_point_on_arc(point: Coords2d, center: Coords2d, start: Coords2d, end: Coords2d, epsilon: f64) -> bool {
675 let radius = ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
677
678 let dist_from_center =
680 ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
681 if (dist_from_center - radius).abs() > epsilon {
682 return false;
683 }
684
685 let start_angle = libm::atan2(start.y - center.y, start.x - center.x);
687 let end_angle = libm::atan2(end.y - center.y, end.x - center.x);
688 let point_angle = libm::atan2(point.y - center.y, point.x - center.x);
689
690 let normalize_angle = |angle: f64| -> f64 {
692 if !angle.is_finite() {
693 return angle;
694 }
695 let mut normalized = angle;
696 while normalized < 0.0 {
697 normalized += TAU;
698 }
699 while normalized >= TAU {
700 normalized -= TAU;
701 }
702 normalized
703 };
704
705 let normalized_start = normalize_angle(start_angle);
706 let normalized_end = normalize_angle(end_angle);
707 let normalized_point = normalize_angle(point_angle);
708
709 if normalized_start < normalized_end {
713 normalized_point >= normalized_start && normalized_point <= normalized_end
715 } else {
716 normalized_point >= normalized_start || normalized_point <= normalized_end
718 }
719}
720
721pub fn line_arc_intersection(
725 line_start: Coords2d,
726 line_end: Coords2d,
727 arc_center: Coords2d,
728 arc_start: Coords2d,
729 arc_end: Coords2d,
730 epsilon: f64,
731) -> Option<Coords2d> {
732 let radius = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
734 + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
735 .sqrt();
736
737 let translated_line_start = Coords2d {
739 x: line_start.x - arc_center.x,
740 y: line_start.y - arc_center.y,
741 };
742 let translated_line_end = Coords2d {
743 x: line_end.x - arc_center.x,
744 y: line_end.y - arc_center.y,
745 };
746
747 let dx = translated_line_end.x - translated_line_start.x;
749 let dy = translated_line_end.y - translated_line_start.y;
750
751 let a = dx * dx + dy * dy;
758 let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
759 let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
760 - radius * radius;
761
762 let discriminant = b * b - 4.0 * a * c;
763
764 if discriminant < 0.0 {
765 return None;
767 }
768
769 if a.abs() < EPSILON_PARALLEL {
770 let dist_from_center = (translated_line_start.x * translated_line_start.x
772 + translated_line_start.y * translated_line_start.y)
773 .sqrt();
774 if (dist_from_center - radius).abs() <= epsilon {
775 let point = line_start;
777 if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
778 return Some(point);
779 }
780 }
781 return None;
782 }
783
784 let sqrt_discriminant = discriminant.sqrt();
785 let t1 = (-b - sqrt_discriminant) / (2.0 * a);
786 let t2 = (-b + sqrt_discriminant) / (2.0 * a);
787
788 let mut candidates: Vec<(f64, Coords2d)> = Vec::new();
790 if (0.0..=1.0).contains(&t1) {
791 let point = Coords2d {
792 x: line_start.x + t1 * (line_end.x - line_start.x),
793 y: line_start.y + t1 * (line_end.y - line_start.y),
794 };
795 candidates.push((t1, point));
796 }
797 if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
798 let point = Coords2d {
799 x: line_start.x + t2 * (line_end.x - line_start.x),
800 y: line_start.y + t2 * (line_end.y - line_start.y),
801 };
802 candidates.push((t2, point));
803 }
804
805 for (_t, point) in candidates {
807 if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
808 return Some(point);
809 }
810 }
811
812 None
813}
814
815pub fn line_circle_intersections(
820 line_start: Coords2d,
821 line_end: Coords2d,
822 circle_center: Coords2d,
823 radius: f64,
824 epsilon: f64,
825) -> Vec<(f64, Coords2d)> {
826 let translated_line_start = Coords2d {
828 x: line_start.x - circle_center.x,
829 y: line_start.y - circle_center.y,
830 };
831 let translated_line_end = Coords2d {
832 x: line_end.x - circle_center.x,
833 y: line_end.y - circle_center.y,
834 };
835
836 let dx = translated_line_end.x - translated_line_start.x;
837 let dy = translated_line_end.y - translated_line_start.y;
838 let a = dx * dx + dy * dy;
839 let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
840 let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
841 - radius * radius;
842
843 if a.abs() < EPSILON_PARALLEL {
844 return Vec::new();
845 }
846
847 let discriminant = b * b - 4.0 * a * c;
848 if discriminant < 0.0 {
849 return Vec::new();
850 }
851
852 let sqrt_discriminant = discriminant.sqrt();
853 let mut intersections = Vec::new();
854
855 let t1 = (-b - sqrt_discriminant) / (2.0 * a);
856 if (0.0..=1.0).contains(&t1) {
857 intersections.push((
858 t1,
859 Coords2d {
860 x: line_start.x + t1 * (line_end.x - line_start.x),
861 y: line_start.y + t1 * (line_end.y - line_start.y),
862 },
863 ));
864 }
865
866 let t2 = (-b + sqrt_discriminant) / (2.0 * a);
867 if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
868 intersections.push((
869 t2,
870 Coords2d {
871 x: line_start.x + t2 * (line_end.x - line_start.x),
872 y: line_start.y + t2 * (line_end.y - line_start.y),
873 },
874 ));
875 }
876
877 intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
878 intersections
879}
880
881pub fn project_point_onto_circle(point: Coords2d, center: Coords2d, start: Coords2d) -> f64 {
887 let normalize_angle = |angle: f64| -> f64 {
888 if !angle.is_finite() {
889 return angle;
890 }
891 let mut normalized = angle;
892 while normalized < 0.0 {
893 normalized += TAU;
894 }
895 while normalized >= TAU {
896 normalized -= TAU;
897 }
898 normalized
899 };
900
901 let start_angle = normalize_angle(libm::atan2(start.y - center.y, start.x - center.x));
902 let point_angle = normalize_angle(libm::atan2(point.y - center.y, point.x - center.x));
903 let delta_ccw = (point_angle - start_angle).rem_euclid(TAU);
904 delta_ccw / TAU
905}
906
907fn is_point_on_circle(point: Coords2d, center: Coords2d, radius: f64, epsilon: f64) -> bool {
908 let dist = ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
909 (dist - radius).abs() <= epsilon
910}
911
912pub fn project_point_onto_arc(point: Coords2d, arc_center: Coords2d, arc_start: Coords2d, arc_end: Coords2d) -> f64 {
915 let start_angle = libm::atan2(arc_start.y - arc_center.y, arc_start.x - arc_center.x);
917 let end_angle = libm::atan2(arc_end.y - arc_center.y, arc_end.x - arc_center.x);
918 let point_angle = libm::atan2(point.y - arc_center.y, point.x - arc_center.x);
919
920 let normalize_angle = |angle: f64| -> f64 {
922 if !angle.is_finite() {
923 return angle;
924 }
925 let mut normalized = angle;
926 while normalized < 0.0 {
927 normalized += TAU;
928 }
929 while normalized >= TAU {
930 normalized -= TAU;
931 }
932 normalized
933 };
934
935 let normalized_start = normalize_angle(start_angle);
936 let normalized_end = normalize_angle(end_angle);
937 let normalized_point = normalize_angle(point_angle);
938
939 let arc_length = if normalized_start < normalized_end {
941 normalized_end - normalized_start
942 } else {
943 TAU - normalized_start + normalized_end
945 };
946
947 if arc_length < EPSILON_PARALLEL {
948 return 0.0;
950 }
951
952 let point_arc_length = if normalized_start < normalized_end {
954 if normalized_point >= normalized_start && normalized_point <= normalized_end {
955 normalized_point - normalized_start
956 } else {
957 let dist_to_start = libm::fmin(
959 (normalized_point - normalized_start).abs(),
960 TAU - (normalized_point - normalized_start).abs(),
961 );
962 let dist_to_end = libm::fmin(
963 (normalized_point - normalized_end).abs(),
964 TAU - (normalized_point - normalized_end).abs(),
965 );
966 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
967 }
968 } else {
969 if normalized_point >= normalized_start || normalized_point <= normalized_end {
971 if normalized_point >= normalized_start {
972 normalized_point - normalized_start
973 } else {
974 TAU - normalized_start + normalized_point
975 }
976 } else {
977 let dist_to_start = libm::fmin(
979 (normalized_point - normalized_start).abs(),
980 TAU - (normalized_point - normalized_start).abs(),
981 );
982 let dist_to_end = libm::fmin(
983 (normalized_point - normalized_end).abs(),
984 TAU - (normalized_point - normalized_end).abs(),
985 );
986 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
987 }
988 };
989
990 point_arc_length / arc_length
992}
993
994pub fn arc_arc_intersections(
998 arc1_center: Coords2d,
999 arc1_start: Coords2d,
1000 arc1_end: Coords2d,
1001 arc2_center: Coords2d,
1002 arc2_start: Coords2d,
1003 arc2_end: Coords2d,
1004 epsilon: f64,
1005) -> Vec<Coords2d> {
1006 let r1 = ((arc1_start.x - arc1_center.x) * (arc1_start.x - arc1_center.x)
1008 + (arc1_start.y - arc1_center.y) * (arc1_start.y - arc1_center.y))
1009 .sqrt();
1010 let r2 = ((arc2_start.x - arc2_center.x) * (arc2_start.x - arc2_center.x)
1011 + (arc2_start.y - arc2_center.y) * (arc2_start.y - arc2_center.y))
1012 .sqrt();
1013
1014 let dx = arc2_center.x - arc1_center.x;
1016 let dy = arc2_center.y - arc1_center.y;
1017 let d = (dx * dx + dy * dy).sqrt();
1018
1019 if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon {
1021 return Vec::new();
1023 }
1024
1025 if d < EPSILON_PARALLEL {
1027 return Vec::new();
1029 }
1030
1031 let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1034 let h_sq = r1 * r1 - a * a;
1035
1036 if h_sq < 0.0 {
1038 return Vec::new();
1039 }
1040
1041 let h = h_sq.sqrt();
1042
1043 if h.is_nan() {
1045 return Vec::new();
1046 }
1047
1048 let ux = dx / d;
1050 let uy = dy / d;
1051
1052 let px = -uy;
1054 let py = ux;
1055
1056 let mid_point = Coords2d {
1058 x: arc1_center.x + a * ux,
1059 y: arc1_center.y + a * uy,
1060 };
1061
1062 let intersection1 = Coords2d {
1064 x: mid_point.x + h * px,
1065 y: mid_point.y + h * py,
1066 };
1067 let intersection2 = Coords2d {
1068 x: mid_point.x - h * px,
1069 y: mid_point.y - h * py,
1070 };
1071
1072 let mut candidates: Vec<Coords2d> = Vec::new();
1074
1075 if is_point_on_arc(intersection1, arc1_center, arc1_start, arc1_end, epsilon)
1076 && is_point_on_arc(intersection1, arc2_center, arc2_start, arc2_end, epsilon)
1077 {
1078 candidates.push(intersection1);
1079 }
1080
1081 if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1082 if is_point_on_arc(intersection2, arc1_center, arc1_start, arc1_end, epsilon)
1084 && is_point_on_arc(intersection2, arc2_center, arc2_start, arc2_end, epsilon)
1085 {
1086 candidates.push(intersection2);
1087 }
1088 }
1089
1090 candidates
1091}
1092
1093pub fn arc_arc_intersection(
1098 arc1_center: Coords2d,
1099 arc1_start: Coords2d,
1100 arc1_end: Coords2d,
1101 arc2_center: Coords2d,
1102 arc2_start: Coords2d,
1103 arc2_end: Coords2d,
1104 epsilon: f64,
1105) -> Option<Coords2d> {
1106 arc_arc_intersections(
1107 arc1_center,
1108 arc1_start,
1109 arc1_end,
1110 arc2_center,
1111 arc2_start,
1112 arc2_end,
1113 epsilon,
1114 )
1115 .first()
1116 .copied()
1117}
1118
1119pub fn circle_arc_intersections(
1123 circle_center: Coords2d,
1124 circle_radius: f64,
1125 arc_center: Coords2d,
1126 arc_start: Coords2d,
1127 arc_end: Coords2d,
1128 epsilon: f64,
1129) -> Vec<Coords2d> {
1130 let r1 = circle_radius;
1131 let r2 = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
1132 + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
1133 .sqrt();
1134
1135 let dx = arc_center.x - circle_center.x;
1136 let dy = arc_center.y - circle_center.y;
1137 let d = (dx * dx + dy * dy).sqrt();
1138
1139 if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon || d < EPSILON_PARALLEL {
1140 return Vec::new();
1141 }
1142
1143 let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1144 let h_sq = r1 * r1 - a * a;
1145 if h_sq < 0.0 {
1146 return Vec::new();
1147 }
1148 let h = h_sq.sqrt();
1149 if h.is_nan() {
1150 return Vec::new();
1151 }
1152
1153 let ux = dx / d;
1154 let uy = dy / d;
1155 let px = -uy;
1156 let py = ux;
1157 let mid_point = Coords2d {
1158 x: circle_center.x + a * ux,
1159 y: circle_center.y + a * uy,
1160 };
1161
1162 let intersection1 = Coords2d {
1163 x: mid_point.x + h * px,
1164 y: mid_point.y + h * py,
1165 };
1166 let intersection2 = Coords2d {
1167 x: mid_point.x - h * px,
1168 y: mid_point.y - h * py,
1169 };
1170
1171 let mut intersections = Vec::new();
1172 if is_point_on_arc(intersection1, arc_center, arc_start, arc_end, epsilon) {
1173 intersections.push(intersection1);
1174 }
1175 if ((intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon)
1176 && is_point_on_arc(intersection2, arc_center, arc_start, arc_end, epsilon)
1177 {
1178 intersections.push(intersection2);
1179 }
1180 intersections
1181}
1182
1183pub fn circle_circle_intersections(
1187 circle1_center: Coords2d,
1188 circle1_radius: f64,
1189 circle2_center: Coords2d,
1190 circle2_radius: f64,
1191 epsilon: f64,
1192) -> Vec<Coords2d> {
1193 let dx = circle2_center.x - circle1_center.x;
1194 let dy = circle2_center.y - circle1_center.y;
1195 let d = (dx * dx + dy * dy).sqrt();
1196
1197 if d > circle1_radius + circle2_radius + epsilon
1198 || d < (circle1_radius - circle2_radius).abs() - epsilon
1199 || d < EPSILON_PARALLEL
1200 {
1201 return Vec::new();
1202 }
1203
1204 let a = (circle1_radius * circle1_radius - circle2_radius * circle2_radius + d * d) / (2.0 * d);
1205 let h_sq = circle1_radius * circle1_radius - a * a;
1206 if h_sq < 0.0 {
1207 return Vec::new();
1208 }
1209
1210 let h = if h_sq <= epsilon { 0.0 } else { h_sq.sqrt() };
1211 if h.is_nan() {
1212 return Vec::new();
1213 }
1214
1215 let ux = dx / d;
1216 let uy = dy / d;
1217 let px = -uy;
1218 let py = ux;
1219
1220 let mid_point = Coords2d {
1221 x: circle1_center.x + a * ux,
1222 y: circle1_center.y + a * uy,
1223 };
1224
1225 let intersection1 = Coords2d {
1226 x: mid_point.x + h * px,
1227 y: mid_point.y + h * py,
1228 };
1229 let intersection2 = Coords2d {
1230 x: mid_point.x - h * px,
1231 y: mid_point.y - h * py,
1232 };
1233
1234 let mut intersections = vec![intersection1];
1235 if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1236 intersections.push(intersection2);
1237 }
1238 intersections
1239}
1240
1241fn get_point_coords_from_native(objects: &[Object], point_id: ObjectId, default_unit: UnitLength) -> Option<Coords2d> {
1244 let point_obj = objects.get(point_id.0)?;
1245
1246 let ObjectKind::Segment { segment } = &point_obj.kind else {
1248 return None;
1249 };
1250
1251 let Segment::Point(point) = segment else {
1252 return None;
1253 };
1254
1255 Some(Coords2d {
1257 x: number_to_unit(&point.position.x, default_unit),
1258 y: number_to_unit(&point.position.y, default_unit),
1259 })
1260}
1261
1262pub fn get_position_coords_for_line(
1265 segment_obj: &Object,
1266 which: LineEndpoint,
1267 objects: &[Object],
1268 default_unit: UnitLength,
1269) -> Option<Coords2d> {
1270 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1271 return None;
1272 };
1273
1274 let Segment::Line(line) = segment else {
1275 return None;
1276 };
1277
1278 let point_id = match which {
1280 LineEndpoint::Start => line.start,
1281 LineEndpoint::End => line.end,
1282 };
1283
1284 get_point_coords_from_native(objects, point_id, default_unit)
1285}
1286
1287fn is_point_coincident_with_segment_native(point_id: ObjectId, segment_id: ObjectId, objects: &[Object]) -> bool {
1289 for obj in objects {
1291 let ObjectKind::Constraint { constraint } = &obj.kind else {
1292 continue;
1293 };
1294
1295 let Constraint::Coincident(coincident) = constraint else {
1296 continue;
1297 };
1298
1299 let has_point = coincident.contains_segment(point_id);
1301 let has_segment = coincident.contains_segment(segment_id);
1302
1303 if has_point && has_segment {
1304 return true;
1305 }
1306 }
1307 false
1308}
1309
1310pub fn get_position_coords_from_arc(
1312 segment_obj: &Object,
1313 which: ArcPoint,
1314 objects: &[Object],
1315 default_unit: UnitLength,
1316) -> Option<Coords2d> {
1317 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1318 return None;
1319 };
1320
1321 let Segment::Arc(arc) = segment else {
1322 return None;
1323 };
1324
1325 let point_id = match which {
1327 ArcPoint::Start => arc.start,
1328 ArcPoint::End => arc.end,
1329 ArcPoint::Center => arc.center,
1330 };
1331
1332 get_point_coords_from_native(objects, point_id, default_unit)
1333}
1334
1335pub fn get_position_coords_from_circle(
1337 segment_obj: &Object,
1338 which: CirclePoint,
1339 objects: &[Object],
1340 default_unit: UnitLength,
1341) -> Option<Coords2d> {
1342 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1343 return None;
1344 };
1345
1346 let Segment::Circle(circle) = segment else {
1347 return None;
1348 };
1349
1350 let point_id = match which {
1351 CirclePoint::Start => circle.start,
1352 CirclePoint::Center => circle.center,
1353 };
1354
1355 get_point_coords_from_native(objects, point_id, default_unit)
1356}
1357
1358#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1360enum CurveKind {
1361 Line,
1362 Circular,
1363}
1364
1365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1367enum CurveDomain {
1368 Open,
1369 Closed,
1370}
1371
1372#[derive(Debug, Clone, Copy)]
1374struct CurveHandle {
1375 segment_id: ObjectId,
1376 kind: CurveKind,
1377 domain: CurveDomain,
1378 start: Coords2d,
1379 end: Coords2d,
1380 center: Option<Coords2d>,
1381 radius: Option<f64>,
1382}
1383
1384impl CurveHandle {
1385 fn project_for_trim(self, point: Coords2d) -> Result<f64, String> {
1386 match (self.kind, self.domain) {
1387 (CurveKind::Line, CurveDomain::Open) => Ok(project_point_onto_segment(point, self.start, self.end)),
1388 (CurveKind::Circular, CurveDomain::Open) => {
1389 let center = self
1390 .center
1391 .ok_or_else(|| format!("Curve {} missing center for arc projection", self.segment_id.0))?;
1392 Ok(project_point_onto_arc(point, center, self.start, self.end))
1393 }
1394 (CurveKind::Circular, CurveDomain::Closed) => {
1395 let center = self
1396 .center
1397 .ok_or_else(|| format!("Curve {} missing center for circle projection", self.segment_id.0))?;
1398 Ok(project_point_onto_circle(point, center, self.start))
1399 }
1400 (CurveKind::Line, CurveDomain::Closed) => Err(format!(
1401 "Invalid curve state: line {} cannot be closed",
1402 self.segment_id.0
1403 )),
1404 }
1405 }
1406}
1407
1408fn load_curve_handle(
1410 segment_obj: &Object,
1411 objects: &[Object],
1412 default_unit: UnitLength,
1413) -> Result<CurveHandle, String> {
1414 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1415 return Err("Object is not a segment".to_owned());
1416 };
1417
1418 match segment {
1419 Segment::Line(_) => {
1420 let start = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit)
1421 .ok_or_else(|| format!("Could not get line start for segment {}", segment_obj.id.0))?;
1422 let end = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit)
1423 .ok_or_else(|| format!("Could not get line end for segment {}", segment_obj.id.0))?;
1424 Ok(CurveHandle {
1425 segment_id: segment_obj.id,
1426 kind: CurveKind::Line,
1427 domain: CurveDomain::Open,
1428 start,
1429 end,
1430 center: None,
1431 radius: None,
1432 })
1433 }
1434 Segment::Arc(_) => {
1435 let start = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit)
1436 .ok_or_else(|| format!("Could not get arc start for segment {}", segment_obj.id.0))?;
1437 let end = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit)
1438 .ok_or_else(|| format!("Could not get arc end for segment {}", segment_obj.id.0))?;
1439 let center = get_position_coords_from_arc(segment_obj, ArcPoint::Center, objects, default_unit)
1440 .ok_or_else(|| format!("Could not get arc center for segment {}", segment_obj.id.0))?;
1441 let radius =
1442 ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1443 Ok(CurveHandle {
1444 segment_id: segment_obj.id,
1445 kind: CurveKind::Circular,
1446 domain: CurveDomain::Open,
1447 start,
1448 end,
1449 center: Some(center),
1450 radius: Some(radius),
1451 })
1452 }
1453 Segment::Circle(_) => {
1454 let start = get_position_coords_from_circle(segment_obj, CirclePoint::Start, objects, default_unit)
1455 .ok_or_else(|| format!("Could not get circle start for segment {}", segment_obj.id.0))?;
1456 let center = get_position_coords_from_circle(segment_obj, CirclePoint::Center, objects, default_unit)
1457 .ok_or_else(|| format!("Could not get circle center for segment {}", segment_obj.id.0))?;
1458 let radius =
1459 ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1460 Ok(CurveHandle {
1461 segment_id: segment_obj.id,
1462 kind: CurveKind::Circular,
1463 domain: CurveDomain::Closed,
1464 start,
1465 end: start,
1467 center: Some(center),
1468 radius: Some(radius),
1469 })
1470 }
1471 Segment::Point(_) => Err(format!(
1472 "Point segment {} cannot be used as trim curve",
1473 segment_obj.id.0
1474 )),
1475 }
1476}
1477
1478fn project_point_onto_curve(curve: CurveHandle, point: Coords2d) -> Result<f64, String> {
1479 curve.project_for_trim(point)
1480}
1481
1482fn curve_contains_point(curve: CurveHandle, point: Coords2d, epsilon: f64) -> bool {
1483 match (curve.kind, curve.domain) {
1484 (CurveKind::Line, CurveDomain::Open) => {
1485 let t = project_point_onto_segment(point, curve.start, curve.end);
1486 (0.0..=1.0).contains(&t) && perpendicular_distance_to_segment(point, curve.start, curve.end) <= epsilon
1487 }
1488 (CurveKind::Circular, CurveDomain::Open) => curve
1489 .center
1490 .is_some_and(|center| is_point_on_arc(point, center, curve.start, curve.end, epsilon)),
1491 (CurveKind::Circular, CurveDomain::Closed) => curve.center.is_some_and(|center| {
1492 let radius = curve
1493 .radius
1494 .unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
1495 is_point_on_circle(point, center, radius, epsilon)
1496 }),
1497 (CurveKind::Line, CurveDomain::Closed) => false,
1498 }
1499}
1500
1501fn curve_line_segment_intersections(
1502 curve: CurveHandle,
1503 line_start: Coords2d,
1504 line_end: Coords2d,
1505 epsilon: f64,
1506) -> Vec<(f64, Coords2d)> {
1507 match (curve.kind, curve.domain) {
1508 (CurveKind::Line, CurveDomain::Open) => {
1509 line_segment_intersection(line_start, line_end, curve.start, curve.end, epsilon)
1510 .map(|intersection| {
1511 (
1512 project_point_onto_segment(intersection, line_start, line_end),
1513 intersection,
1514 )
1515 })
1516 .into_iter()
1517 .collect()
1518 }
1519 (CurveKind::Circular, CurveDomain::Open) => curve
1520 .center
1521 .and_then(|center| line_arc_intersection(line_start, line_end, center, curve.start, curve.end, epsilon))
1522 .map(|intersection| {
1523 (
1524 project_point_onto_segment(intersection, line_start, line_end),
1525 intersection,
1526 )
1527 })
1528 .into_iter()
1529 .collect(),
1530 (CurveKind::Circular, CurveDomain::Closed) => {
1531 let Some(center) = curve.center else {
1532 return Vec::new();
1533 };
1534 let radius = curve
1535 .radius
1536 .unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
1537 line_circle_intersections(line_start, line_end, center, radius, epsilon)
1538 }
1539 (CurveKind::Line, CurveDomain::Closed) => Vec::new(),
1540 }
1541}
1542
1543fn curve_polyline_intersections(curve: CurveHandle, polyline: &[Coords2d], epsilon: f64) -> Vec<(Coords2d, usize)> {
1544 let mut intersections = Vec::new();
1545
1546 for i in 0..polyline.len().saturating_sub(1) {
1547 let p1 = polyline[i];
1548 let p2 = polyline[i + 1];
1549 for (_, intersection) in curve_line_segment_intersections(curve, p1, p2, epsilon) {
1550 intersections.push((intersection, i));
1551 }
1552 }
1553
1554 intersections
1555}
1556
1557fn curve_curve_intersections(curve: CurveHandle, other: CurveHandle, epsilon: f64) -> Vec<Coords2d> {
1558 match (curve.kind, curve.domain, other.kind, other.domain) {
1559 (CurveKind::Line, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => {
1560 line_segment_intersection(curve.start, curve.end, other.start, other.end, epsilon)
1561 .into_iter()
1562 .collect()
1563 }
1564 (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => other
1565 .center
1566 .and_then(|other_center| {
1567 line_arc_intersection(curve.start, curve.end, other_center, other.start, other.end, epsilon)
1568 })
1569 .into_iter()
1570 .collect(),
1571 (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
1572 let Some(other_center) = other.center else {
1573 return Vec::new();
1574 };
1575 let other_radius = other.radius.unwrap_or_else(|| {
1576 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1577 });
1578 line_circle_intersections(curve.start, curve.end, other_center, other_radius, epsilon)
1579 .into_iter()
1580 .map(|(_, point)| point)
1581 .collect()
1582 }
1583 (CurveKind::Circular, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => curve
1584 .center
1585 .and_then(|curve_center| {
1586 line_arc_intersection(other.start, other.end, curve_center, curve.start, curve.end, epsilon)
1587 })
1588 .into_iter()
1589 .collect(),
1590 (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => {
1591 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1592 return Vec::new();
1593 };
1594 arc_arc_intersections(
1595 curve_center,
1596 curve.start,
1597 curve.end,
1598 other_center,
1599 other.start,
1600 other.end,
1601 epsilon,
1602 )
1603 }
1604 (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
1605 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1606 return Vec::new();
1607 };
1608 let other_radius = other.radius.unwrap_or_else(|| {
1609 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1610 });
1611 circle_arc_intersections(
1612 other_center,
1613 other_radius,
1614 curve_center,
1615 curve.start,
1616 curve.end,
1617 epsilon,
1618 )
1619 }
1620 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Line, CurveDomain::Open) => {
1621 let Some(curve_center) = curve.center else {
1622 return Vec::new();
1623 };
1624 let curve_radius = curve.radius.unwrap_or_else(|| {
1625 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1626 });
1627 line_circle_intersections(other.start, other.end, curve_center, curve_radius, epsilon)
1628 .into_iter()
1629 .map(|(_, point)| point)
1630 .collect()
1631 }
1632 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Open) => {
1633 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1634 return Vec::new();
1635 };
1636 let curve_radius = curve.radius.unwrap_or_else(|| {
1637 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1638 });
1639 circle_arc_intersections(
1640 curve_center,
1641 curve_radius,
1642 other_center,
1643 other.start,
1644 other.end,
1645 epsilon,
1646 )
1647 }
1648 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Closed) => {
1649 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1650 return Vec::new();
1651 };
1652 let curve_radius = curve.radius.unwrap_or_else(|| {
1653 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1654 });
1655 let other_radius = other.radius.unwrap_or_else(|| {
1656 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1657 });
1658 circle_circle_intersections(curve_center, curve_radius, other_center, other_radius, epsilon)
1659 }
1660 _ => Vec::new(),
1661 }
1662}
1663
1664fn segment_endpoint_points(
1665 segment_obj: &Object,
1666 objects: &[Object],
1667 default_unit: UnitLength,
1668) -> Vec<(ObjectId, Coords2d)> {
1669 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1670 return Vec::new();
1671 };
1672
1673 match segment {
1674 Segment::Line(line) => {
1675 let mut points = Vec::new();
1676 if let Some(start) = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit) {
1677 points.push((line.start, start));
1678 }
1679 if let Some(end) = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit) {
1680 points.push((line.end, end));
1681 }
1682 points
1683 }
1684 Segment::Arc(arc) => {
1685 let mut points = Vec::new();
1686 if let Some(start) = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit) {
1687 points.push((arc.start, start));
1688 }
1689 if let Some(end) = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit) {
1690 points.push((arc.end, end));
1691 }
1692 points
1693 }
1694 _ => Vec::new(),
1695 }
1696}
1697
1698pub fn get_next_trim_spawn(
1726 points: &[Coords2d],
1727 start_index: usize,
1728 objects: &[Object],
1729 default_unit: UnitLength,
1730) -> TrimItem {
1731 let scene_curves: Vec<CurveHandle> = objects
1732 .iter()
1733 .filter_map(|obj| load_curve_handle(obj, objects, default_unit).ok())
1734 .collect();
1735
1736 for i in start_index..points.len().saturating_sub(1) {
1738 let p1 = points[i];
1739 let p2 = points[i + 1];
1740
1741 for curve in &scene_curves {
1743 let intersections = curve_line_segment_intersections(*curve, p1, p2, EPSILON_POINT_ON_SEGMENT);
1744 if let Some((_, intersection)) = intersections.first() {
1745 return TrimItem::Spawn {
1746 trim_spawn_seg_id: curve.segment_id,
1747 trim_spawn_coords: *intersection,
1748 next_index: i,
1749 };
1750 }
1751 }
1752 }
1753
1754 TrimItem::None {
1756 next_index: points.len().saturating_sub(1),
1757 }
1758}
1759
1760pub fn get_trim_spawn_terminations(
1815 trim_spawn_seg_id: ObjectId,
1816 trim_spawn_coords: &[Coords2d],
1817 objects: &[Object],
1818 default_unit: UnitLength,
1819) -> Result<TrimTerminations, String> {
1820 let trim_spawn_seg = objects.iter().find(|obj| obj.id == trim_spawn_seg_id);
1822
1823 let trim_spawn_seg = match trim_spawn_seg {
1824 Some(seg) => seg,
1825 None => {
1826 return Err(format!("Trim spawn segment {} not found", trim_spawn_seg_id.0));
1827 }
1828 };
1829
1830 let trim_curve = load_curve_handle(trim_spawn_seg, objects, default_unit).map_err(|e| {
1831 format!(
1832 "Failed to load trim spawn segment {} as normalized curve: {}",
1833 trim_spawn_seg_id.0, e
1834 )
1835 })?;
1836
1837 let all_intersections = curve_polyline_intersections(trim_curve, trim_spawn_coords, EPSILON_POINT_ON_SEGMENT);
1842
1843 let intersection_point = if all_intersections.is_empty() {
1846 return Err("Could not find intersection point between polyline and trim spawn segment".to_string());
1847 } else {
1848 let mid_index = (trim_spawn_coords.len() - 1) / 2;
1850 let mid_point = trim_spawn_coords[mid_index];
1851
1852 let mut min_dist = f64::INFINITY;
1854 let mut closest_intersection = all_intersections[0].0;
1855
1856 for (intersection, _) in &all_intersections {
1857 let dist = ((intersection.x - mid_point.x) * (intersection.x - mid_point.x)
1858 + (intersection.y - mid_point.y) * (intersection.y - mid_point.y))
1859 .sqrt();
1860 if dist < min_dist {
1861 min_dist = dist;
1862 closest_intersection = *intersection;
1863 }
1864 }
1865
1866 closest_intersection
1867 };
1868
1869 let intersection_t = project_point_onto_curve(trim_curve, intersection_point)?;
1871
1872 let left_termination = find_termination_in_direction(
1874 trim_spawn_seg,
1875 trim_curve,
1876 intersection_t,
1877 TrimDirection::Left,
1878 objects,
1879 default_unit,
1880 )?;
1881
1882 let right_termination = find_termination_in_direction(
1883 trim_spawn_seg,
1884 trim_curve,
1885 intersection_t,
1886 TrimDirection::Right,
1887 objects,
1888 default_unit,
1889 )?;
1890
1891 Ok(TrimTerminations {
1892 left_side: left_termination,
1893 right_side: right_termination,
1894 })
1895}
1896
1897fn find_termination_in_direction(
1950 trim_spawn_seg: &Object,
1951 trim_curve: CurveHandle,
1952 intersection_t: f64,
1953 direction: TrimDirection,
1954 objects: &[Object],
1955 default_unit: UnitLength,
1956) -> Result<TrimTermination, String> {
1957 let ObjectKind::Segment { segment } = &trim_spawn_seg.kind else {
1959 return Err("Trim spawn segment is not a segment".to_string());
1960 };
1961
1962 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1964 enum CandidateType {
1965 Intersection,
1966 Coincident,
1967 Endpoint,
1968 }
1969
1970 #[derive(Debug, Clone)]
1971 struct Candidate {
1972 t: f64,
1973 point: Coords2d,
1974 candidate_type: CandidateType,
1975 segment_id: Option<ObjectId>,
1976 point_id: Option<ObjectId>,
1977 }
1978
1979 let mut candidates: Vec<Candidate> = Vec::new();
1980
1981 match segment {
1983 Segment::Line(line) => {
1984 candidates.push(Candidate {
1985 t: 0.0,
1986 point: trim_curve.start,
1987 candidate_type: CandidateType::Endpoint,
1988 segment_id: None,
1989 point_id: Some(line.start),
1990 });
1991 candidates.push(Candidate {
1992 t: 1.0,
1993 point: trim_curve.end,
1994 candidate_type: CandidateType::Endpoint,
1995 segment_id: None,
1996 point_id: Some(line.end),
1997 });
1998 }
1999 Segment::Arc(arc) => {
2000 candidates.push(Candidate {
2002 t: 0.0,
2003 point: trim_curve.start,
2004 candidate_type: CandidateType::Endpoint,
2005 segment_id: None,
2006 point_id: Some(arc.start),
2007 });
2008 candidates.push(Candidate {
2009 t: 1.0,
2010 point: trim_curve.end,
2011 candidate_type: CandidateType::Endpoint,
2012 segment_id: None,
2013 point_id: Some(arc.end),
2014 });
2015 }
2016 Segment::Circle(_) => {
2017 }
2019 _ => {}
2020 }
2021
2022 let trim_spawn_seg_id = trim_spawn_seg.id;
2024
2025 for other_seg in objects.iter() {
2027 let other_id = other_seg.id;
2028 if other_id == trim_spawn_seg_id {
2029 continue;
2030 }
2031
2032 if let Ok(other_curve) = load_curve_handle(other_seg, objects, default_unit) {
2033 for intersection in curve_curve_intersections(trim_curve, other_curve, EPSILON_POINT_ON_SEGMENT) {
2034 let Ok(t) = project_point_onto_curve(trim_curve, intersection) else {
2035 continue;
2036 };
2037 candidates.push(Candidate {
2038 t,
2039 point: intersection,
2040 candidate_type: CandidateType::Intersection,
2041 segment_id: Some(other_id),
2042 point_id: None,
2043 });
2044 }
2045 }
2046
2047 for (other_point_id, other_point) in segment_endpoint_points(other_seg, objects, default_unit) {
2048 if !is_point_coincident_with_segment_native(other_point_id, trim_spawn_seg_id, objects) {
2049 continue;
2050 }
2051 if !curve_contains_point(trim_curve, other_point, EPSILON_POINT_ON_SEGMENT) {
2052 continue;
2053 }
2054 let Ok(t) = project_point_onto_curve(trim_curve, other_point) else {
2055 continue;
2056 };
2057 candidates.push(Candidate {
2058 t,
2059 point: other_point,
2060 candidate_type: CandidateType::Coincident,
2061 segment_id: Some(other_id),
2062 point_id: Some(other_point_id),
2063 });
2064 }
2065 }
2066
2067 let is_circle_segment = trim_curve.domain == CurveDomain::Closed;
2068
2069 let intersection_epsilon = EPSILON_POINT_ON_SEGMENT * 10.0; let direction_distance = |candidate_t: f64| -> f64 {
2073 if is_circle_segment {
2074 match direction {
2075 TrimDirection::Left => (intersection_t - candidate_t).rem_euclid(1.0),
2076 TrimDirection::Right => (candidate_t - intersection_t).rem_euclid(1.0),
2077 }
2078 } else {
2079 (candidate_t - intersection_t).abs()
2080 }
2081 };
2082 let filtered_candidates: Vec<Candidate> = candidates
2083 .into_iter()
2084 .filter(|candidate| {
2085 let dist_from_intersection = if is_circle_segment {
2086 let ccw = (candidate.t - intersection_t).rem_euclid(1.0);
2087 let cw = (intersection_t - candidate.t).rem_euclid(1.0);
2088 libm::fmin(ccw, cw)
2089 } else {
2090 (candidate.t - intersection_t).abs()
2091 };
2092 if dist_from_intersection < intersection_epsilon {
2093 return false; }
2095
2096 if is_circle_segment {
2097 direction_distance(candidate.t) > intersection_epsilon
2098 } else {
2099 match direction {
2100 TrimDirection::Left => candidate.t < intersection_t,
2101 TrimDirection::Right => candidate.t > intersection_t,
2102 }
2103 }
2104 })
2105 .collect();
2106
2107 let mut sorted_candidates = filtered_candidates;
2110 sorted_candidates.sort_by(|a, b| {
2111 let dist_a = direction_distance(a.t);
2112 let dist_b = direction_distance(b.t);
2113 let dist_diff = dist_a - dist_b;
2114 if dist_diff.abs() > EPSILON_POINT_ON_SEGMENT {
2115 dist_diff.partial_cmp(&0.0).unwrap_or(std::cmp::Ordering::Equal)
2116 } else {
2117 let type_priority = |candidate_type: CandidateType| -> i32 {
2119 match candidate_type {
2120 CandidateType::Coincident => 0,
2121 CandidateType::Intersection => 1,
2122 CandidateType::Endpoint => 2,
2123 }
2124 };
2125 type_priority(a.candidate_type).cmp(&type_priority(b.candidate_type))
2126 }
2127 });
2128
2129 let closest_candidate = match sorted_candidates.first() {
2131 Some(c) => c,
2132 None => {
2133 if is_circle_segment {
2134 return Err("No trim termination candidate found for circle".to_string());
2135 }
2136 let endpoint = match direction {
2138 TrimDirection::Left => trim_curve.start,
2139 TrimDirection::Right => trim_curve.end,
2140 };
2141 return Ok(TrimTermination::SegEndPoint {
2142 trim_termination_coords: endpoint,
2143 });
2144 }
2145 };
2146
2147 if !is_circle_segment
2151 && closest_candidate.candidate_type == CandidateType::Intersection
2152 && let Some(seg_id) = closest_candidate.segment_id
2153 {
2154 let intersecting_seg = objects.iter().find(|obj| obj.id == seg_id);
2155
2156 if let Some(intersecting_seg) = intersecting_seg {
2157 let endpoint_epsilon = EPSILON_POINT_ON_SEGMENT * 1000.0; let is_other_seg_endpoint = segment_endpoint_points(intersecting_seg, objects, default_unit)
2160 .into_iter()
2161 .any(|(_, endpoint)| {
2162 let dist_to_endpoint = ((closest_candidate.point.x - endpoint.x).powi(2)
2163 + (closest_candidate.point.y - endpoint.y).powi(2))
2164 .sqrt();
2165 dist_to_endpoint < endpoint_epsilon
2166 });
2167
2168 if is_other_seg_endpoint {
2171 let endpoint = match direction {
2172 TrimDirection::Left => trim_curve.start,
2173 TrimDirection::Right => trim_curve.end,
2174 };
2175 return Ok(TrimTermination::SegEndPoint {
2176 trim_termination_coords: endpoint,
2177 });
2178 }
2179 }
2180
2181 let endpoint_t = match direction {
2183 TrimDirection::Left => 0.0,
2184 TrimDirection::Right => 1.0,
2185 };
2186 let endpoint = match direction {
2187 TrimDirection::Left => trim_curve.start,
2188 TrimDirection::Right => trim_curve.end,
2189 };
2190 let dist_to_endpoint_param = (closest_candidate.t - endpoint_t).abs();
2191 let dist_to_endpoint_coords = ((closest_candidate.point.x - endpoint.x)
2192 * (closest_candidate.point.x - endpoint.x)
2193 + (closest_candidate.point.y - endpoint.y) * (closest_candidate.point.y - endpoint.y))
2194 .sqrt();
2195
2196 let is_at_endpoint =
2197 dist_to_endpoint_param < EPSILON_POINT_ON_SEGMENT || dist_to_endpoint_coords < EPSILON_POINT_ON_SEGMENT;
2198
2199 if is_at_endpoint {
2200 return Ok(TrimTermination::SegEndPoint {
2202 trim_termination_coords: endpoint,
2203 });
2204 }
2205 }
2206
2207 let endpoint_t_for_return = match direction {
2209 TrimDirection::Left => 0.0,
2210 TrimDirection::Right => 1.0,
2211 };
2212 if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Intersection {
2213 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2214 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2215 let endpoint = match direction {
2218 TrimDirection::Left => trim_curve.start,
2219 TrimDirection::Right => trim_curve.end,
2220 };
2221 return Ok(TrimTermination::SegEndPoint {
2222 trim_termination_coords: endpoint,
2223 });
2224 }
2225 }
2226
2227 let endpoint = match direction {
2229 TrimDirection::Left => trim_curve.start,
2230 TrimDirection::Right => trim_curve.end,
2231 };
2232 if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Endpoint {
2233 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2234 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2235 return Ok(TrimTermination::SegEndPoint {
2237 trim_termination_coords: endpoint,
2238 });
2239 }
2240 }
2241
2242 if closest_candidate.candidate_type == CandidateType::Coincident {
2244 Ok(TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2246 trim_termination_coords: closest_candidate.point,
2247 intersecting_seg_id: closest_candidate
2248 .segment_id
2249 .ok_or_else(|| "Missing segment_id for coincident".to_string())?,
2250 other_segment_point_id: closest_candidate
2251 .point_id
2252 .ok_or_else(|| "Missing point_id for coincident".to_string())?,
2253 })
2254 } else if closest_candidate.candidate_type == CandidateType::Intersection {
2255 Ok(TrimTermination::Intersection {
2256 trim_termination_coords: closest_candidate.point,
2257 intersecting_seg_id: closest_candidate
2258 .segment_id
2259 .ok_or_else(|| "Missing segment_id for intersection".to_string())?,
2260 })
2261 } else {
2262 if is_circle_segment {
2263 return Err("Circle trim termination unexpectedly resolved to endpoint".to_string());
2264 }
2265 Ok(TrimTermination::SegEndPoint {
2267 trim_termination_coords: closest_candidate.point,
2268 })
2269 }
2270}
2271
2272#[cfg(test)]
2283#[allow(dead_code)]
2284pub(crate) async fn execute_trim_loop<F, Fut>(
2285 points: &[Coords2d],
2286 default_unit: UnitLength,
2287 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2288 mut execute_operations: F,
2289) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>
2290where
2291 F: FnMut(Vec<TrimOperation>, crate::frontend::api::SceneGraphDelta) -> Fut,
2292 Fut: std::future::Future<
2293 Output = Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>,
2294 >,
2295{
2296 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2298 let points = normalized_points.as_slice();
2299
2300 let mut start_index = 0;
2301 let max_iterations = 1000;
2302 let mut iteration_count = 0;
2303 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2304 crate::frontend::api::SourceDelta { text: String::new() },
2305 initial_scene_graph_delta.clone(),
2306 ));
2307 let mut invalidates_ids = false;
2308 let mut current_scene_graph_delta = initial_scene_graph_delta;
2309 let circle_delete_fallback_strategy =
2310 |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
2311 if !error.contains("No trim termination candidate found for circle") {
2312 return None;
2313 }
2314 let is_circle = scene_objects
2315 .iter()
2316 .find(|obj| obj.id == segment_id)
2317 .is_some_and(|obj| {
2318 matches!(
2319 obj.kind,
2320 ObjectKind::Segment {
2321 segment: Segment::Circle(_)
2322 }
2323 )
2324 });
2325 if is_circle {
2326 Some(vec![TrimOperation::SimpleTrim {
2327 segment_to_trim_id: segment_id,
2328 }])
2329 } else {
2330 None
2331 }
2332 };
2333
2334 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2335 iteration_count += 1;
2336
2337 let next_trim_spawn = get_next_trim_spawn(
2339 points,
2340 start_index,
2341 ¤t_scene_graph_delta.new_graph.objects,
2342 default_unit,
2343 );
2344
2345 match &next_trim_spawn {
2346 TrimItem::None { next_index } => {
2347 let old_start_index = start_index;
2348 start_index = *next_index;
2349
2350 if start_index <= old_start_index {
2352 start_index = old_start_index + 1;
2353 }
2354
2355 if start_index >= points.len().saturating_sub(1) {
2357 break;
2358 }
2359 continue;
2360 }
2361 TrimItem::Spawn {
2362 trim_spawn_seg_id,
2363 trim_spawn_coords,
2364 next_index,
2365 ..
2366 } => {
2367 let terminations = match get_trim_spawn_terminations(
2369 *trim_spawn_seg_id,
2370 points,
2371 ¤t_scene_graph_delta.new_graph.objects,
2372 default_unit,
2373 ) {
2374 Ok(terms) => terms,
2375 Err(e) => {
2376 crate::logln!("Error getting trim spawn terminations: {}", e);
2377 if let Some(strategy) = circle_delete_fallback_strategy(
2378 &e,
2379 *trim_spawn_seg_id,
2380 ¤t_scene_graph_delta.new_graph.objects,
2381 ) {
2382 match execute_operations(strategy, current_scene_graph_delta.clone()).await {
2383 Ok((source_delta, scene_graph_delta)) => {
2384 last_result = Some((source_delta, scene_graph_delta.clone()));
2385 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2386 current_scene_graph_delta = scene_graph_delta;
2387 }
2388 Err(exec_err) => {
2389 crate::logln!(
2390 "Error executing circle-delete fallback trim operation: {}",
2391 exec_err
2392 );
2393 }
2394 }
2395
2396 let old_start_index = start_index;
2397 start_index = *next_index;
2398 if start_index <= old_start_index {
2399 start_index = old_start_index + 1;
2400 }
2401 continue;
2402 }
2403
2404 let old_start_index = start_index;
2405 start_index = *next_index;
2406 if start_index <= old_start_index {
2407 start_index = old_start_index + 1;
2408 }
2409 continue;
2410 }
2411 };
2412
2413 let trim_spawn_segment = current_scene_graph_delta
2415 .new_graph
2416 .objects
2417 .iter()
2418 .find(|obj| obj.id == *trim_spawn_seg_id)
2419 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
2420
2421 let plan = match build_trim_plan(
2422 *trim_spawn_seg_id,
2423 *trim_spawn_coords,
2424 trim_spawn_segment,
2425 &terminations.left_side,
2426 &terminations.right_side,
2427 ¤t_scene_graph_delta.new_graph.objects,
2428 default_unit,
2429 ) {
2430 Ok(plan) => plan,
2431 Err(e) => {
2432 crate::logln!("Error determining trim strategy: {}", e);
2433 let old_start_index = start_index;
2434 start_index = *next_index;
2435 if start_index <= old_start_index {
2436 start_index = old_start_index + 1;
2437 }
2438 continue;
2439 }
2440 };
2441 let strategy = lower_trim_plan(&plan);
2442
2443 let geometry_was_modified = trim_plan_modifies_geometry(&plan);
2446
2447 match execute_operations(strategy, current_scene_graph_delta.clone()).await {
2449 Ok((source_delta, scene_graph_delta)) => {
2450 last_result = Some((source_delta, scene_graph_delta.clone()));
2451 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2452 current_scene_graph_delta = scene_graph_delta;
2453 }
2454 Err(e) => {
2455 crate::logln!("Error executing trim operations: {}", e);
2456 }
2458 }
2459
2460 let old_start_index = start_index;
2462 start_index = *next_index;
2463
2464 if start_index <= old_start_index && !geometry_was_modified {
2466 start_index = old_start_index + 1;
2467 }
2468 }
2469 }
2470 }
2471
2472 if iteration_count >= max_iterations {
2473 return Err(format!("Reached max iterations ({})", max_iterations));
2474 }
2475
2476 last_result.ok_or_else(|| "No trim operations were executed".to_string())
2478}
2479
2480#[cfg(all(feature = "artifact-graph", test))]
2482#[derive(Debug, Clone)]
2483pub struct TrimFlowResult {
2484 pub kcl_code: String,
2485 pub invalidates_ids: bool,
2486}
2487
2488#[cfg(all(not(target_arch = "wasm32"), feature = "artifact-graph", test))]
2504pub(crate) async fn execute_trim_flow(
2505 kcl_code: &str,
2506 trim_points: &[Coords2d],
2507 sketch_id: ObjectId,
2508) -> Result<TrimFlowResult, String> {
2509 use crate::ExecutorContext;
2510 use crate::Program;
2511 use crate::execution::MockConfig;
2512 use crate::frontend::FrontendState;
2513 use crate::frontend::api::Version;
2514
2515 let parse_result = Program::parse(kcl_code).map_err(|e| format!("Failed to parse KCL: {}", e))?;
2517 let (program_opt, errors) = parse_result;
2518 if !errors.is_empty() {
2519 return Err(format!("Failed to parse KCL: {:?}", errors));
2520 }
2521 let program = program_opt.ok_or_else(|| "No AST produced".to_string())?;
2522
2523 let mock_ctx = ExecutorContext::new_mock(None).await;
2524
2525 let result = async {
2527 let mut frontend = FrontendState::new();
2528
2529 frontend.program = program.clone();
2531
2532 let exec_outcome = mock_ctx
2533 .run_mock(&program, &MockConfig::default())
2534 .await
2535 .map_err(|e| format!("Failed to execute program: {}", e.error.message()))?;
2536
2537 let exec_outcome = frontend.update_state_after_exec(exec_outcome, false);
2538 #[allow(unused_mut)] let mut initial_scene_graph = frontend.scene_graph.clone();
2540
2541 #[cfg(feature = "artifact-graph")]
2544 if initial_scene_graph.objects.is_empty() && !exec_outcome.scene_objects.is_empty() {
2545 initial_scene_graph.objects = exec_outcome.scene_objects.clone();
2546 }
2547
2548 let actual_sketch_id = if let Some(sketch_mode) = initial_scene_graph.sketch_mode {
2551 sketch_mode
2552 } else {
2553 initial_scene_graph
2555 .objects
2556 .iter()
2557 .find(|obj| matches!(obj.kind, crate::frontend::api::ObjectKind::Sketch { .. }))
2558 .map(|obj| obj.id)
2559 .unwrap_or(sketch_id) };
2561
2562 let version = Version(0);
2563 let initial_scene_graph_delta = crate::frontend::api::SceneGraphDelta {
2564 new_graph: initial_scene_graph,
2565 new_objects: vec![],
2566 invalidates_ids: false,
2567 exec_outcome,
2568 };
2569
2570 let (source_delta, scene_graph_delta) = execute_trim_loop_with_context(
2575 trim_points,
2576 initial_scene_graph_delta,
2577 &mut frontend,
2578 &mock_ctx,
2579 version,
2580 actual_sketch_id,
2581 )
2582 .await?;
2583
2584 if source_delta.text.is_empty() {
2587 return Err("No trim operations were executed - source delta is empty".to_string());
2588 }
2589
2590 Ok(TrimFlowResult {
2591 kcl_code: source_delta.text,
2592 invalidates_ids: scene_graph_delta.invalidates_ids,
2593 })
2594 }
2595 .await;
2596
2597 mock_ctx.close().await;
2599
2600 result
2601}
2602
2603pub async fn execute_trim_loop_with_context(
2609 points: &[Coords2d],
2610 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2611 frontend: &mut crate::frontend::FrontendState,
2612 ctx: &crate::ExecutorContext,
2613 version: crate::frontend::api::Version,
2614 sketch_id: ObjectId,
2615) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
2616 let default_unit = frontend.default_length_unit();
2618 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2619
2620 let mut current_scene_graph_delta = initial_scene_graph_delta.clone();
2623 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2624 crate::frontend::api::SourceDelta { text: String::new() },
2625 initial_scene_graph_delta.clone(),
2626 ));
2627 let mut invalidates_ids = false;
2628 let mut start_index = 0;
2629 let max_iterations = 1000;
2630 let mut iteration_count = 0;
2631 let circle_delete_fallback_strategy =
2632 |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
2633 if !error.contains("No trim termination candidate found for circle") {
2634 return None;
2635 }
2636 let is_circle = scene_objects
2637 .iter()
2638 .find(|obj| obj.id == segment_id)
2639 .is_some_and(|obj| {
2640 matches!(
2641 obj.kind,
2642 ObjectKind::Segment {
2643 segment: Segment::Circle(_)
2644 }
2645 )
2646 });
2647 if is_circle {
2648 Some(vec![TrimOperation::SimpleTrim {
2649 segment_to_trim_id: segment_id,
2650 }])
2651 } else {
2652 None
2653 }
2654 };
2655
2656 let points = normalized_points.as_slice();
2657
2658 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2659 iteration_count += 1;
2660
2661 let next_trim_spawn = get_next_trim_spawn(
2663 points,
2664 start_index,
2665 ¤t_scene_graph_delta.new_graph.objects,
2666 default_unit,
2667 );
2668
2669 match &next_trim_spawn {
2670 TrimItem::None { next_index } => {
2671 let old_start_index = start_index;
2672 start_index = *next_index;
2673 if start_index <= old_start_index {
2674 start_index = old_start_index + 1;
2675 }
2676 if start_index >= points.len().saturating_sub(1) {
2677 break;
2678 }
2679 continue;
2680 }
2681 TrimItem::Spawn {
2682 trim_spawn_seg_id,
2683 trim_spawn_coords,
2684 next_index,
2685 ..
2686 } => {
2687 let terminations = match get_trim_spawn_terminations(
2689 *trim_spawn_seg_id,
2690 points,
2691 ¤t_scene_graph_delta.new_graph.objects,
2692 default_unit,
2693 ) {
2694 Ok(terms) => terms,
2695 Err(e) => {
2696 crate::logln!("Error getting trim spawn terminations: {}", e);
2697 if let Some(strategy) = circle_delete_fallback_strategy(
2698 &e,
2699 *trim_spawn_seg_id,
2700 ¤t_scene_graph_delta.new_graph.objects,
2701 ) {
2702 match execute_trim_operations_simple(
2703 strategy.clone(),
2704 ¤t_scene_graph_delta,
2705 frontend,
2706 ctx,
2707 version,
2708 sketch_id,
2709 )
2710 .await
2711 {
2712 Ok((source_delta, scene_graph_delta)) => {
2713 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2714 last_result = Some((source_delta, scene_graph_delta.clone()));
2715 current_scene_graph_delta = scene_graph_delta;
2716 }
2717 Err(exec_err) => {
2718 crate::logln!(
2719 "Error executing circle-delete fallback trim operation: {}",
2720 exec_err
2721 );
2722 }
2723 }
2724
2725 let old_start_index = start_index;
2726 start_index = *next_index;
2727 if start_index <= old_start_index {
2728 start_index = old_start_index + 1;
2729 }
2730 continue;
2731 }
2732
2733 let old_start_index = start_index;
2734 start_index = *next_index;
2735 if start_index <= old_start_index {
2736 start_index = old_start_index + 1;
2737 }
2738 continue;
2739 }
2740 };
2741
2742 let trim_spawn_segment = current_scene_graph_delta
2744 .new_graph
2745 .objects
2746 .iter()
2747 .find(|obj| obj.id == *trim_spawn_seg_id)
2748 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
2749
2750 let plan = match build_trim_plan(
2751 *trim_spawn_seg_id,
2752 *trim_spawn_coords,
2753 trim_spawn_segment,
2754 &terminations.left_side,
2755 &terminations.right_side,
2756 ¤t_scene_graph_delta.new_graph.objects,
2757 default_unit,
2758 ) {
2759 Ok(plan) => plan,
2760 Err(e) => {
2761 crate::logln!("Error determining trim strategy: {}", e);
2762 let old_start_index = start_index;
2763 start_index = *next_index;
2764 if start_index <= old_start_index {
2765 start_index = old_start_index + 1;
2766 }
2767 continue;
2768 }
2769 };
2770 let strategy = lower_trim_plan(&plan);
2771
2772 let geometry_was_modified = trim_plan_modifies_geometry(&plan);
2775
2776 match execute_trim_operations_simple(
2778 strategy.clone(),
2779 ¤t_scene_graph_delta,
2780 frontend,
2781 ctx,
2782 version,
2783 sketch_id,
2784 )
2785 .await
2786 {
2787 Ok((source_delta, scene_graph_delta)) => {
2788 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2789 last_result = Some((source_delta, scene_graph_delta.clone()));
2790 current_scene_graph_delta = scene_graph_delta;
2791 }
2792 Err(e) => {
2793 crate::logln!("Error executing trim operations: {}", e);
2794 }
2795 }
2796
2797 let old_start_index = start_index;
2799 start_index = *next_index;
2800 if start_index <= old_start_index && !geometry_was_modified {
2801 start_index = old_start_index + 1;
2802 }
2803 }
2804 }
2805 }
2806
2807 if iteration_count >= max_iterations {
2808 return Err(format!("Reached max iterations ({})", max_iterations));
2809 }
2810
2811 let (source_delta, mut scene_graph_delta) =
2812 last_result.ok_or_else(|| "No trim operations were executed".to_string())?;
2813 scene_graph_delta.invalidates_ids = invalidates_ids;
2815 Ok((source_delta, scene_graph_delta))
2816}
2817
2818pub(crate) fn build_trim_plan(
2878 trim_spawn_id: ObjectId,
2879 trim_spawn_coords: Coords2d,
2880 trim_spawn_segment: &Object,
2881 left_side: &TrimTermination,
2882 right_side: &TrimTermination,
2883 objects: &[Object],
2884 default_unit: UnitLength,
2885) -> Result<TrimPlan, String> {
2886 if matches!(left_side, TrimTermination::SegEndPoint { .. })
2888 && matches!(right_side, TrimTermination::SegEndPoint { .. })
2889 {
2890 return Ok(TrimPlan::DeleteSegment {
2891 segment_id: trim_spawn_id,
2892 });
2893 }
2894
2895 let is_intersect_or_coincident = |side: &TrimTermination| -> bool {
2897 matches!(
2898 side,
2899 TrimTermination::Intersection { .. }
2900 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
2901 )
2902 };
2903
2904 let left_side_needs_tail_cut = is_intersect_or_coincident(left_side) && !is_intersect_or_coincident(right_side);
2905 let right_side_needs_tail_cut = is_intersect_or_coincident(right_side) && !is_intersect_or_coincident(left_side);
2906
2907 let ObjectKind::Segment { segment } = &trim_spawn_segment.kind else {
2909 return Err("Trim spawn segment is not a segment".to_string());
2910 };
2911
2912 let (_segment_type, ctor) = match segment {
2913 Segment::Line(line) => ("Line", &line.ctor),
2914 Segment::Arc(arc) => ("Arc", &arc.ctor),
2915 Segment::Circle(circle) => ("Circle", &circle.ctor),
2916 _ => {
2917 return Err("Trim spawn segment is not a Line, Arc, or Circle".to_string());
2918 }
2919 };
2920
2921 let units = match ctor {
2923 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
2924 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2925 _ => NumericSuffix::Mm,
2926 },
2927 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
2928 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2929 _ => NumericSuffix::Mm,
2930 },
2931 SegmentCtor::Circle(circle_ctor) => match &circle_ctor.start.x {
2932 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2933 _ => NumericSuffix::Mm,
2934 },
2935 _ => NumericSuffix::Mm,
2936 };
2937
2938 let find_distance_constraints_for_segment = |segment_id: ObjectId| -> Vec<ObjectId> {
2940 let mut constraint_ids = Vec::new();
2941 for obj in objects {
2942 let ObjectKind::Constraint { constraint } = &obj.kind else {
2943 continue;
2944 };
2945
2946 let Constraint::Distance(distance) = constraint else {
2947 continue;
2948 };
2949
2950 let points_owned_by_segment: Vec<bool> = distance
2956 .point_ids()
2957 .map(|point_id| {
2958 if let Some(point_obj) = objects.iter().find(|o| o.id == point_id)
2959 && let ObjectKind::Segment { segment } = &point_obj.kind
2960 && let Segment::Point(point) = segment
2961 && let Some(owner_id) = point.owner
2962 {
2963 return owner_id == segment_id;
2964 }
2965 false
2966 })
2967 .collect();
2968
2969 if points_owned_by_segment.len() == 2 && points_owned_by_segment.iter().all(|&owned| owned) {
2971 constraint_ids.push(obj.id);
2972 }
2973 }
2974 constraint_ids
2975 };
2976
2977 let find_existing_point_segment_coincident =
2979 |trim_seg_id: ObjectId, intersecting_seg_id: ObjectId| -> CoincidentData {
2980 let lookup_by_point_id = |point_id: ObjectId| -> Option<CoincidentData> {
2982 for obj in objects {
2983 let ObjectKind::Constraint { constraint } = &obj.kind else {
2984 continue;
2985 };
2986
2987 let Constraint::Coincident(coincident) = constraint else {
2988 continue;
2989 };
2990
2991 let involves_trim_seg = coincident.segment_ids().any(|id| id == trim_seg_id || id == point_id);
2992 let involves_point = coincident.contains_segment(point_id);
2993
2994 if involves_trim_seg && involves_point {
2995 return Some(CoincidentData {
2996 intersecting_seg_id,
2997 intersecting_endpoint_point_id: Some(point_id),
2998 existing_point_segment_constraint_id: Some(obj.id),
2999 });
3000 }
3001 }
3002 None
3003 };
3004
3005 let trim_seg = objects.iter().find(|obj| obj.id == trim_seg_id);
3007
3008 let mut trim_endpoint_ids: Vec<ObjectId> = Vec::new();
3009 if let Some(seg) = trim_seg
3010 && let ObjectKind::Segment { segment } = &seg.kind
3011 {
3012 match segment {
3013 Segment::Line(line) => {
3014 trim_endpoint_ids.push(line.start);
3015 trim_endpoint_ids.push(line.end);
3016 }
3017 Segment::Arc(arc) => {
3018 trim_endpoint_ids.push(arc.start);
3019 trim_endpoint_ids.push(arc.end);
3020 }
3021 _ => {}
3022 }
3023 }
3024
3025 let intersecting_obj = objects.iter().find(|obj| obj.id == intersecting_seg_id);
3026
3027 if let Some(obj) = intersecting_obj
3028 && let ObjectKind::Segment { segment } = &obj.kind
3029 && let Segment::Point(_) = segment
3030 && let Some(found) = lookup_by_point_id(intersecting_seg_id)
3031 {
3032 return found;
3033 }
3034
3035 let mut intersecting_endpoint_ids: Vec<ObjectId> = Vec::new();
3037 if let Some(obj) = intersecting_obj
3038 && let ObjectKind::Segment { segment } = &obj.kind
3039 {
3040 match segment {
3041 Segment::Line(line) => {
3042 intersecting_endpoint_ids.push(line.start);
3043 intersecting_endpoint_ids.push(line.end);
3044 }
3045 Segment::Arc(arc) => {
3046 intersecting_endpoint_ids.push(arc.start);
3047 intersecting_endpoint_ids.push(arc.end);
3048 }
3049 _ => {}
3050 }
3051 }
3052
3053 intersecting_endpoint_ids.push(intersecting_seg_id);
3055
3056 for obj in objects {
3058 let ObjectKind::Constraint { constraint } = &obj.kind else {
3059 continue;
3060 };
3061
3062 let Constraint::Coincident(coincident) = constraint else {
3063 continue;
3064 };
3065
3066 let constraint_segment_ids: Vec<ObjectId> = coincident.get_segments();
3067
3068 let involves_trim_seg = constraint_segment_ids.contains(&trim_seg_id)
3070 || trim_endpoint_ids.iter().any(|&id| constraint_segment_ids.contains(&id));
3071
3072 if !involves_trim_seg {
3073 continue;
3074 }
3075
3076 if let Some(&intersecting_endpoint_id) = intersecting_endpoint_ids
3078 .iter()
3079 .find(|&&id| constraint_segment_ids.contains(&id))
3080 {
3081 return CoincidentData {
3082 intersecting_seg_id,
3083 intersecting_endpoint_point_id: Some(intersecting_endpoint_id),
3084 existing_point_segment_constraint_id: Some(obj.id),
3085 };
3086 }
3087 }
3088
3089 CoincidentData {
3091 intersecting_seg_id,
3092 intersecting_endpoint_point_id: None,
3093 existing_point_segment_constraint_id: None,
3094 }
3095 };
3096
3097 let find_point_segment_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<serde_json::Value> {
3099 let mut constraints: Vec<serde_json::Value> = Vec::new();
3100 for obj in objects {
3101 let ObjectKind::Constraint { constraint } = &obj.kind else {
3102 continue;
3103 };
3104
3105 let Constraint::Coincident(coincident) = constraint else {
3106 continue;
3107 };
3108
3109 if !coincident.contains_segment(endpoint_point_id) {
3111 continue;
3112 }
3113
3114 let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
3116
3117 if let Some(other_id) = other_segment_id
3118 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
3119 {
3120 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
3122 constraints.push(serde_json::json!({
3123 "constraintId": obj.id.0,
3124 "segmentOrPointId": other_id.0,
3125 }));
3126 }
3127 }
3128 }
3129 constraints
3130 };
3131
3132 let find_point_point_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
3135 let mut constraint_ids = Vec::new();
3136 for obj in objects {
3137 let ObjectKind::Constraint { constraint } = &obj.kind else {
3138 continue;
3139 };
3140
3141 let Constraint::Coincident(coincident) = constraint else {
3142 continue;
3143 };
3144
3145 if !coincident.contains_segment(endpoint_point_id) {
3147 continue;
3148 }
3149
3150 let is_point_point = coincident.segment_ids().all(|seg_id| {
3152 if let Some(seg_obj) = objects.iter().find(|o| o.id == seg_id) {
3153 matches!(&seg_obj.kind, ObjectKind::Segment { segment } if matches!(segment, Segment::Point(_)))
3154 } else {
3155 false
3156 }
3157 });
3158
3159 if is_point_point {
3160 constraint_ids.push(obj.id);
3161 }
3162 }
3163 constraint_ids
3164 };
3165
3166 let find_point_segment_coincident_constraint_ids = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
3169 let mut constraint_ids = Vec::new();
3170 for obj in objects {
3171 let ObjectKind::Constraint { constraint } = &obj.kind else {
3172 continue;
3173 };
3174
3175 let Constraint::Coincident(coincident) = constraint else {
3176 continue;
3177 };
3178
3179 if !coincident.contains_segment(endpoint_point_id) {
3181 continue;
3182 }
3183
3184 let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
3186
3187 if let Some(other_id) = other_segment_id
3188 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
3189 {
3190 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
3192 constraint_ids.push(obj.id);
3193 }
3194 }
3195 }
3196 constraint_ids
3197 };
3198
3199 if left_side_needs_tail_cut || right_side_needs_tail_cut {
3201 let side = if left_side_needs_tail_cut {
3202 left_side
3203 } else {
3204 right_side
3205 };
3206
3207 let intersection_coords = match side {
3208 TrimTermination::Intersection {
3209 trim_termination_coords,
3210 ..
3211 }
3212 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3213 trim_termination_coords,
3214 ..
3215 } => *trim_termination_coords,
3216 TrimTermination::SegEndPoint { .. } => {
3217 return Err("Logic error: side should not be segEndPoint here".to_string());
3218 }
3219 };
3220
3221 let endpoint_to_change = if left_side_needs_tail_cut {
3222 EndpointChanged::End
3223 } else {
3224 EndpointChanged::Start
3225 };
3226
3227 let intersecting_seg_id = match side {
3228 TrimTermination::Intersection {
3229 intersecting_seg_id, ..
3230 }
3231 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3232 intersecting_seg_id, ..
3233 } => *intersecting_seg_id,
3234 TrimTermination::SegEndPoint { .. } => {
3235 return Err("Logic error".to_string());
3236 }
3237 };
3238
3239 let mut coincident_data = if matches!(
3240 side,
3241 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3242 ) {
3243 let point_id = match side {
3244 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3245 other_segment_point_id, ..
3246 } => *other_segment_point_id,
3247 _ => return Err("Logic error".to_string()),
3248 };
3249 let mut data = find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id);
3250 data.intersecting_endpoint_point_id = Some(point_id);
3251 data
3252 } else {
3253 find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id)
3254 };
3255
3256 let trim_seg = objects.iter().find(|obj| obj.id == trim_spawn_id);
3258
3259 let endpoint_point_id = if let Some(seg) = trim_seg {
3260 let ObjectKind::Segment { segment } = &seg.kind else {
3261 return Err("Trim spawn segment is not a segment".to_string());
3262 };
3263 match segment {
3264 Segment::Line(line) => {
3265 if endpoint_to_change == EndpointChanged::Start {
3266 Some(line.start)
3267 } else {
3268 Some(line.end)
3269 }
3270 }
3271 Segment::Arc(arc) => {
3272 if endpoint_to_change == EndpointChanged::Start {
3273 Some(arc.start)
3274 } else {
3275 Some(arc.end)
3276 }
3277 }
3278 _ => None,
3279 }
3280 } else {
3281 None
3282 };
3283
3284 if let (Some(endpoint_id), Some(existing_constraint_id)) =
3285 (endpoint_point_id, coincident_data.existing_point_segment_constraint_id)
3286 {
3287 let constraint_involves_trimmed_endpoint = objects
3288 .iter()
3289 .find(|obj| obj.id == existing_constraint_id)
3290 .and_then(|obj| match &obj.kind {
3291 ObjectKind::Constraint {
3292 constraint: Constraint::Coincident(coincident),
3293 } => Some(coincident.contains_segment(endpoint_id) || coincident.contains_segment(trim_spawn_id)),
3294 _ => None,
3295 })
3296 .unwrap_or(false);
3297
3298 if !constraint_involves_trimmed_endpoint {
3299 coincident_data.existing_point_segment_constraint_id = None;
3300 coincident_data.intersecting_endpoint_point_id = None;
3301 }
3302 }
3303
3304 let coincident_end_constraint_to_delete_ids = if let Some(point_id) = endpoint_point_id {
3306 let mut constraint_ids = find_point_point_coincident_constraints(point_id);
3307 constraint_ids.extend(find_point_segment_coincident_constraint_ids(point_id));
3309 constraint_ids
3310 } else {
3311 Vec::new()
3312 };
3313
3314 let point_axis_constraint_ids_to_delete = if let Some(point_id) = endpoint_point_id {
3315 objects
3316 .iter()
3317 .filter_map(|obj| {
3318 let ObjectKind::Constraint { constraint } = &obj.kind else {
3319 return None;
3320 };
3321
3322 point_axis_constraint_references_point(constraint, point_id).then_some(obj.id)
3323 })
3324 .collect::<Vec<_>>()
3325 } else {
3326 Vec::new()
3327 };
3328
3329 let new_ctor = match ctor {
3331 SegmentCtor::Line(line_ctor) => {
3332 let new_point = crate::frontend::sketch::Point2d {
3334 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
3335 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
3336 };
3337 if endpoint_to_change == EndpointChanged::Start {
3338 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3339 start: new_point,
3340 end: line_ctor.end.clone(),
3341 construction: line_ctor.construction,
3342 })
3343 } else {
3344 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3345 start: line_ctor.start.clone(),
3346 end: new_point,
3347 construction: line_ctor.construction,
3348 })
3349 }
3350 }
3351 SegmentCtor::Arc(arc_ctor) => {
3352 let new_point = crate::frontend::sketch::Point2d {
3354 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
3355 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
3356 };
3357 if endpoint_to_change == EndpointChanged::Start {
3358 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3359 start: new_point,
3360 end: arc_ctor.end.clone(),
3361 center: arc_ctor.center.clone(),
3362 construction: arc_ctor.construction,
3363 })
3364 } else {
3365 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3366 start: arc_ctor.start.clone(),
3367 end: new_point,
3368 center: arc_ctor.center.clone(),
3369 construction: arc_ctor.construction,
3370 })
3371 }
3372 }
3373 _ => {
3374 return Err("Unsupported segment type for edit".to_string());
3375 }
3376 };
3377
3378 let mut all_constraint_ids_to_delete: Vec<ObjectId> = Vec::new();
3380 if let Some(constraint_id) = coincident_data.existing_point_segment_constraint_id {
3381 all_constraint_ids_to_delete.push(constraint_id);
3382 }
3383 all_constraint_ids_to_delete.extend(coincident_end_constraint_to_delete_ids);
3384 all_constraint_ids_to_delete.extend(point_axis_constraint_ids_to_delete);
3385
3386 let distance_constraint_ids = find_distance_constraints_for_segment(trim_spawn_id);
3389 all_constraint_ids_to_delete.extend(distance_constraint_ids);
3390
3391 return Ok(TrimPlan::TailCut {
3392 segment_id: trim_spawn_id,
3393 endpoint_changed: endpoint_to_change,
3394 ctor: new_ctor,
3395 segment_or_point_to_make_coincident_to: intersecting_seg_id,
3396 intersecting_endpoint_point_id: coincident_data.intersecting_endpoint_point_id,
3397 constraint_ids_to_delete: all_constraint_ids_to_delete,
3398 });
3399 }
3400
3401 if matches!(segment, Segment::Circle(_)) {
3404 let left_side_intersects = is_intersect_or_coincident(left_side);
3405 let right_side_intersects = is_intersect_or_coincident(right_side);
3406 if !(left_side_intersects && right_side_intersects) {
3407 return Err(format!(
3408 "Unsupported circle trim termination combination: left={:?} right={:?}",
3409 left_side, right_side
3410 ));
3411 }
3412
3413 let left_trim_coords = match left_side {
3414 TrimTermination::SegEndPoint {
3415 trim_termination_coords,
3416 }
3417 | TrimTermination::Intersection {
3418 trim_termination_coords,
3419 ..
3420 }
3421 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3422 trim_termination_coords,
3423 ..
3424 } => *trim_termination_coords,
3425 };
3426 let right_trim_coords = match right_side {
3427 TrimTermination::SegEndPoint {
3428 trim_termination_coords,
3429 }
3430 | TrimTermination::Intersection {
3431 trim_termination_coords,
3432 ..
3433 }
3434 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3435 trim_termination_coords,
3436 ..
3437 } => *trim_termination_coords,
3438 };
3439
3440 let trim_points_coincident = ((left_trim_coords.x - right_trim_coords.x)
3443 * (left_trim_coords.x - right_trim_coords.x)
3444 + (left_trim_coords.y - right_trim_coords.y) * (left_trim_coords.y - right_trim_coords.y))
3445 .sqrt()
3446 <= EPSILON_POINT_ON_SEGMENT * 10.0;
3447 if trim_points_coincident {
3448 return Ok(TrimPlan::DeleteSegment {
3449 segment_id: trim_spawn_id,
3450 });
3451 }
3452
3453 let circle_center_coords =
3454 get_position_coords_from_circle(trim_spawn_segment, CirclePoint::Center, objects, default_unit)
3455 .ok_or_else(|| {
3456 format!(
3457 "Could not get center coordinates for circle segment {}",
3458 trim_spawn_id.0
3459 )
3460 })?;
3461
3462 let spawn_on_left_to_right = is_point_on_arc(
3464 trim_spawn_coords,
3465 circle_center_coords,
3466 left_trim_coords,
3467 right_trim_coords,
3468 EPSILON_POINT_ON_SEGMENT,
3469 );
3470 let (arc_start_coords, arc_end_coords, arc_start_termination, arc_end_termination) = if spawn_on_left_to_right {
3471 (
3472 right_trim_coords,
3473 left_trim_coords,
3474 Box::new(right_side.clone()),
3475 Box::new(left_side.clone()),
3476 )
3477 } else {
3478 (
3479 left_trim_coords,
3480 right_trim_coords,
3481 Box::new(left_side.clone()),
3482 Box::new(right_side.clone()),
3483 )
3484 };
3485
3486 return Ok(TrimPlan::ReplaceCircleWithArc {
3487 circle_id: trim_spawn_id,
3488 arc_start_coords,
3489 arc_end_coords,
3490 arc_start_termination,
3491 arc_end_termination,
3492 });
3493 }
3494
3495 let left_side_intersects = is_intersect_or_coincident(left_side);
3497 let right_side_intersects = is_intersect_or_coincident(right_side);
3498
3499 if left_side_intersects && right_side_intersects {
3500 let left_intersecting_seg_id = match left_side {
3503 TrimTermination::Intersection {
3504 intersecting_seg_id, ..
3505 }
3506 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3507 intersecting_seg_id, ..
3508 } => *intersecting_seg_id,
3509 TrimTermination::SegEndPoint { .. } => {
3510 return Err("Logic error: left side should not be segEndPoint".to_string());
3511 }
3512 };
3513
3514 let right_intersecting_seg_id = match right_side {
3515 TrimTermination::Intersection {
3516 intersecting_seg_id, ..
3517 }
3518 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3519 intersecting_seg_id, ..
3520 } => *intersecting_seg_id,
3521 TrimTermination::SegEndPoint { .. } => {
3522 return Err("Logic error: right side should not be segEndPoint".to_string());
3523 }
3524 };
3525
3526 let left_coincident_data = if matches!(
3527 left_side,
3528 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3529 ) {
3530 let point_id = match left_side {
3531 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3532 other_segment_point_id, ..
3533 } => *other_segment_point_id,
3534 _ => return Err("Logic error".to_string()),
3535 };
3536 let mut data = find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id);
3537 data.intersecting_endpoint_point_id = Some(point_id);
3538 data
3539 } else {
3540 find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id)
3541 };
3542
3543 let right_coincident_data = if matches!(
3544 right_side,
3545 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3546 ) {
3547 let point_id = match right_side {
3548 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3549 other_segment_point_id, ..
3550 } => *other_segment_point_id,
3551 _ => return Err("Logic error".to_string()),
3552 };
3553 let mut data = find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id);
3554 data.intersecting_endpoint_point_id = Some(point_id);
3555 data
3556 } else {
3557 find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id)
3558 };
3559
3560 let (original_start_point_id, original_end_point_id) = match segment {
3562 Segment::Line(line) => (Some(line.start), Some(line.end)),
3563 Segment::Arc(arc) => (Some(arc.start), Some(arc.end)),
3564 _ => (None, None),
3565 };
3566
3567 let original_end_point_coords = match segment {
3569 Segment::Line(_) => {
3570 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
3571 }
3572 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
3573 _ => None,
3574 };
3575
3576 let Some(original_end_coords) = original_end_point_coords else {
3577 return Err(
3578 "Could not get original end point coordinates before editing - this is required for split trim"
3579 .to_string(),
3580 );
3581 };
3582
3583 let left_trim_coords = match left_side {
3585 TrimTermination::SegEndPoint {
3586 trim_termination_coords,
3587 }
3588 | TrimTermination::Intersection {
3589 trim_termination_coords,
3590 ..
3591 }
3592 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3593 trim_termination_coords,
3594 ..
3595 } => *trim_termination_coords,
3596 };
3597
3598 let right_trim_coords = match right_side {
3599 TrimTermination::SegEndPoint {
3600 trim_termination_coords,
3601 }
3602 | TrimTermination::Intersection {
3603 trim_termination_coords,
3604 ..
3605 }
3606 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3607 trim_termination_coords,
3608 ..
3609 } => *trim_termination_coords,
3610 };
3611
3612 let dist_to_original_end = ((right_trim_coords.x - original_end_coords.x)
3614 * (right_trim_coords.x - original_end_coords.x)
3615 + (right_trim_coords.y - original_end_coords.y) * (right_trim_coords.y - original_end_coords.y))
3616 .sqrt();
3617 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3618 return Err(
3619 "Split point is at original end point - this should be handled as cutTail, not split".to_string(),
3620 );
3621 }
3622
3623 let mut constraints_to_migrate: Vec<ConstraintToMigrate> = Vec::new();
3626 let mut constraints_to_delete_set: IndexSet<ObjectId> = IndexSet::new();
3627
3628 if let Some(constraint_id) = left_coincident_data.existing_point_segment_constraint_id {
3630 constraints_to_delete_set.insert(constraint_id);
3631 }
3632 if let Some(constraint_id) = right_coincident_data.existing_point_segment_constraint_id {
3633 constraints_to_delete_set.insert(constraint_id);
3634 }
3635
3636 if let Some(end_id) = original_end_point_id {
3637 for obj in objects {
3638 let ObjectKind::Constraint { constraint } = &obj.kind else {
3639 continue;
3640 };
3641
3642 if point_axis_constraint_references_point(constraint, end_id) {
3643 constraints_to_delete_set.insert(obj.id);
3644 }
3645 }
3646 }
3647
3648 if let Some(end_id) = original_end_point_id {
3650 let end_point_point_constraint_ids = find_point_point_coincident_constraints(end_id);
3651 for constraint_id in end_point_point_constraint_ids {
3652 let other_point_id_opt = objects.iter().find_map(|obj| {
3654 if obj.id != constraint_id {
3655 return None;
3656 }
3657 let ObjectKind::Constraint { constraint } = &obj.kind else {
3658 return None;
3659 };
3660 let Constraint::Coincident(coincident) = constraint else {
3661 return None;
3662 };
3663 coincident.segment_ids().find(|&seg_id| seg_id != end_id)
3664 });
3665
3666 if let Some(other_point_id) = other_point_id_opt {
3667 constraints_to_delete_set.insert(constraint_id);
3668 constraints_to_migrate.push(ConstraintToMigrate {
3670 constraint_id,
3671 other_entity_id: other_point_id,
3672 is_point_point: true,
3673 attach_to_endpoint: AttachToEndpoint::End,
3674 });
3675 }
3676 }
3677 }
3678
3679 if let Some(end_id) = original_end_point_id {
3681 let end_point_segment_constraints = find_point_segment_coincident_constraints(end_id);
3682 for constraint_json in end_point_segment_constraints {
3683 if let Some(constraint_id_usize) = constraint_json
3684 .get("constraintId")
3685 .and_then(|v| v.as_u64())
3686 .map(|id| id as usize)
3687 {
3688 let constraint_id = ObjectId(constraint_id_usize);
3689 constraints_to_delete_set.insert(constraint_id);
3690 if let Some(other_id_usize) = constraint_json
3692 .get("segmentOrPointId")
3693 .and_then(|v| v.as_u64())
3694 .map(|id| id as usize)
3695 {
3696 constraints_to_migrate.push(ConstraintToMigrate {
3697 constraint_id,
3698 other_entity_id: ObjectId(other_id_usize),
3699 is_point_point: false,
3700 attach_to_endpoint: AttachToEndpoint::End,
3701 });
3702 }
3703 }
3704 }
3705 }
3706
3707 if let Some(end_id) = original_end_point_id {
3712 for obj in objects {
3713 let ObjectKind::Constraint { constraint } = &obj.kind else {
3714 continue;
3715 };
3716
3717 let Constraint::Coincident(coincident) = constraint else {
3718 continue;
3719 };
3720
3721 if !coincident.contains_segment(trim_spawn_id) {
3726 continue;
3727 }
3728 if let (Some(start_id), Some(end_id_val)) = (original_start_point_id, Some(end_id))
3731 && coincident.segment_ids().any(|id| id == start_id || id == end_id_val)
3732 {
3733 continue; }
3735
3736 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
3738
3739 if let Some(other_id) = other_id {
3740 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3742 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3743 continue;
3744 };
3745
3746 let Segment::Point(point) = other_segment else {
3747 continue;
3748 };
3749
3750 let point_coords = Coords2d {
3752 x: number_to_unit(&point.position.x, default_unit),
3753 y: number_to_unit(&point.position.y, default_unit),
3754 };
3755
3756 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
3759 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
3760 if let ObjectKind::Segment {
3761 segment: Segment::Point(end_point),
3762 } = &end_point_obj.kind
3763 {
3764 Some(Coords2d {
3765 x: number_to_unit(&end_point.position.x, default_unit),
3766 y: number_to_unit(&end_point.position.y, default_unit),
3767 })
3768 } else {
3769 None
3770 }
3771 } else {
3772 None
3773 }
3774 } else {
3775 None
3776 };
3777
3778 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
3779 let dist_to_original_end = ((point_coords.x - reference_coords.x)
3780 * (point_coords.x - reference_coords.x)
3781 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
3782 .sqrt();
3783
3784 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3785 let has_point_point_constraint = find_point_point_coincident_constraints(end_id)
3788 .iter()
3789 .any(|&constraint_id| {
3790 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
3791 if let ObjectKind::Constraint {
3792 constraint: Constraint::Coincident(coincident),
3793 } = &constraint_obj.kind
3794 {
3795 coincident.contains_segment(other_id)
3796 } else {
3797 false
3798 }
3799 } else {
3800 false
3801 }
3802 });
3803
3804 if !has_point_point_constraint {
3805 constraints_to_migrate.push(ConstraintToMigrate {
3807 constraint_id: obj.id,
3808 other_entity_id: other_id,
3809 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
3812 }
3813 constraints_to_delete_set.insert(obj.id);
3815 }
3816 }
3817 }
3818 }
3819 }
3820
3821 let split_point = right_trim_coords; let segment_start_coords = match segment {
3826 Segment::Line(_) => {
3827 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::Start, objects, default_unit)
3828 }
3829 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Start, objects, default_unit),
3830 _ => None,
3831 };
3832 let segment_end_coords = match segment {
3833 Segment::Line(_) => {
3834 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
3835 }
3836 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
3837 _ => None,
3838 };
3839 let segment_center_coords = match segment {
3840 Segment::Line(_) => None,
3841 Segment::Arc(_) => {
3842 get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Center, objects, default_unit)
3843 }
3844 _ => None,
3845 };
3846
3847 if let (Some(start_coords), Some(end_coords)) = (segment_start_coords, segment_end_coords) {
3848 let split_point_t_opt = match segment {
3850 Segment::Line(_) => Some(project_point_onto_segment(split_point, start_coords, end_coords)),
3851 Segment::Arc(_) => segment_center_coords
3852 .map(|center| project_point_onto_arc(split_point, center, start_coords, end_coords)),
3853 _ => None,
3854 };
3855
3856 if let Some(split_point_t) = split_point_t_opt {
3857 for obj in objects {
3859 let ObjectKind::Constraint { constraint } = &obj.kind else {
3860 continue;
3861 };
3862
3863 let Constraint::Coincident(coincident) = constraint else {
3864 continue;
3865 };
3866
3867 if !coincident.contains_segment(trim_spawn_id) {
3869 continue;
3870 }
3871
3872 if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id)
3874 && coincident.segment_ids().any(|id| id == start_id || id == end_id)
3875 {
3876 continue;
3877 }
3878
3879 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
3881
3882 if let Some(other_id) = other_id {
3883 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3885 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3886 continue;
3887 };
3888
3889 let Segment::Point(point) = other_segment else {
3890 continue;
3891 };
3892
3893 let point_coords = Coords2d {
3895 x: number_to_unit(&point.position.x, default_unit),
3896 y: number_to_unit(&point.position.y, default_unit),
3897 };
3898
3899 let point_t = match segment {
3901 Segment::Line(_) => project_point_onto_segment(point_coords, start_coords, end_coords),
3902 Segment::Arc(_) => {
3903 if let Some(center) = segment_center_coords {
3904 project_point_onto_arc(point_coords, center, start_coords, end_coords)
3905 } else {
3906 continue; }
3908 }
3909 _ => continue, };
3911
3912 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
3915 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
3916 if let ObjectKind::Segment {
3917 segment: Segment::Point(end_point),
3918 } = &end_point_obj.kind
3919 {
3920 Some(Coords2d {
3921 x: number_to_unit(&end_point.position.x, default_unit),
3922 y: number_to_unit(&end_point.position.y, default_unit),
3923 })
3924 } else {
3925 None
3926 }
3927 } else {
3928 None
3929 }
3930 } else {
3931 None
3932 };
3933
3934 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
3935 let dist_to_original_end = ((point_coords.x - reference_coords.x)
3936 * (point_coords.x - reference_coords.x)
3937 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
3938 .sqrt();
3939
3940 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3941 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
3945 find_point_point_coincident_constraints(end_id)
3946 .iter()
3947 .any(|&constraint_id| {
3948 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
3949 {
3950 if let ObjectKind::Constraint {
3951 constraint: Constraint::Coincident(coincident),
3952 } = &constraint_obj.kind
3953 {
3954 coincident.contains_segment(other_id)
3955 } else {
3956 false
3957 }
3958 } else {
3959 false
3960 }
3961 })
3962 } else {
3963 false
3964 };
3965
3966 if !has_point_point_constraint {
3967 constraints_to_migrate.push(ConstraintToMigrate {
3969 constraint_id: obj.id,
3970 other_entity_id: other_id,
3971 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
3974 }
3975 constraints_to_delete_set.insert(obj.id);
3977 continue; }
3979
3980 let dist_to_start = ((point_coords.x - start_coords.x) * (point_coords.x - start_coords.x)
3982 + (point_coords.y - start_coords.y) * (point_coords.y - start_coords.y))
3983 .sqrt();
3984 let is_at_start = (point_t - 0.0).abs() < EPSILON_POINT_ON_SEGMENT
3985 || dist_to_start < EPSILON_POINT_ON_SEGMENT;
3986
3987 if is_at_start {
3988 continue; }
3990
3991 let dist_to_split = (point_t - split_point_t).abs();
3993 if dist_to_split < EPSILON_POINT_ON_SEGMENT * 100.0 {
3994 continue; }
3996
3997 if point_t > split_point_t {
3999 constraints_to_migrate.push(ConstraintToMigrate {
4000 constraint_id: obj.id,
4001 other_entity_id: other_id,
4002 is_point_point: false, attach_to_endpoint: AttachToEndpoint::Segment, });
4005 constraints_to_delete_set.insert(obj.id);
4006 }
4007 }
4008 }
4009 }
4010 } } let distance_constraint_ids_for_split = find_distance_constraints_for_segment(trim_spawn_id);
4018
4019 let arc_center_point_id: Option<ObjectId> = match segment {
4021 Segment::Arc(arc) => Some(arc.center),
4022 _ => None,
4023 };
4024
4025 for constraint_id in distance_constraint_ids_for_split {
4026 if let Some(center_id) = arc_center_point_id {
4028 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
4030 && let ObjectKind::Constraint { constraint } = &constraint_obj.kind
4031 && let Constraint::Distance(distance) = constraint
4032 && distance.contains_point(center_id)
4033 {
4034 continue;
4036 }
4037 }
4038
4039 constraints_to_delete_set.insert(constraint_id);
4040 }
4041
4042 for obj in objects {
4050 let ObjectKind::Constraint { constraint } = &obj.kind else {
4051 continue;
4052 };
4053
4054 let Constraint::Coincident(coincident) = constraint else {
4055 continue;
4056 };
4057
4058 if !coincident.contains_segment(trim_spawn_id) {
4060 continue;
4061 }
4062
4063 if constraints_to_delete_set.contains(&obj.id) {
4065 continue;
4066 }
4067
4068 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
4075
4076 if let Some(other_id) = other_id {
4077 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
4079 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
4080 continue;
4081 };
4082
4083 let Segment::Point(point) = other_segment else {
4084 continue;
4085 };
4086
4087 let _is_endpoint_constraint =
4090 if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id) {
4091 coincident.segment_ids().any(|id| id == start_id || id == end_id)
4092 } else {
4093 false
4094 };
4095
4096 let point_coords = Coords2d {
4098 x: number_to_unit(&point.position.x, default_unit),
4099 y: number_to_unit(&point.position.y, default_unit),
4100 };
4101
4102 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
4104 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
4105 if let ObjectKind::Segment {
4106 segment: Segment::Point(end_point),
4107 } = &end_point_obj.kind
4108 {
4109 Some(Coords2d {
4110 x: number_to_unit(&end_point.position.x, default_unit),
4111 y: number_to_unit(&end_point.position.y, default_unit),
4112 })
4113 } else {
4114 None
4115 }
4116 } else {
4117 None
4118 }
4119 } else {
4120 None
4121 };
4122
4123 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
4124 let dist_to_original_end = ((point_coords.x - reference_coords.x)
4125 * (point_coords.x - reference_coords.x)
4126 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
4127 .sqrt();
4128
4129 let is_at_original_end = dist_to_original_end < EPSILON_POINT_ON_SEGMENT * 2.0;
4132
4133 if is_at_original_end {
4134 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
4137 find_point_point_coincident_constraints(end_id)
4138 .iter()
4139 .any(|&constraint_id| {
4140 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
4141 if let ObjectKind::Constraint {
4142 constraint: Constraint::Coincident(coincident),
4143 } = &constraint_obj.kind
4144 {
4145 coincident.contains_segment(other_id)
4146 } else {
4147 false
4148 }
4149 } else {
4150 false
4151 }
4152 })
4153 } else {
4154 false
4155 };
4156
4157 if !has_point_point_constraint {
4158 constraints_to_migrate.push(ConstraintToMigrate {
4160 constraint_id: obj.id,
4161 other_entity_id: other_id,
4162 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
4165 }
4166 constraints_to_delete_set.insert(obj.id);
4168 }
4169 }
4170 }
4171 }
4172
4173 let constraints_to_delete: Vec<ObjectId> = constraints_to_delete_set.iter().copied().collect();
4175 let plan = TrimPlan::SplitSegment {
4176 segment_id: trim_spawn_id,
4177 left_trim_coords,
4178 right_trim_coords,
4179 original_end_coords,
4180 left_side: Box::new(left_side.clone()),
4181 right_side: Box::new(right_side.clone()),
4182 left_side_coincident_data: CoincidentData {
4183 intersecting_seg_id: left_intersecting_seg_id,
4184 intersecting_endpoint_point_id: left_coincident_data.intersecting_endpoint_point_id,
4185 existing_point_segment_constraint_id: left_coincident_data.existing_point_segment_constraint_id,
4186 },
4187 right_side_coincident_data: CoincidentData {
4188 intersecting_seg_id: right_intersecting_seg_id,
4189 intersecting_endpoint_point_id: right_coincident_data.intersecting_endpoint_point_id,
4190 existing_point_segment_constraint_id: right_coincident_data.existing_point_segment_constraint_id,
4191 },
4192 constraints_to_migrate,
4193 constraints_to_delete,
4194 };
4195
4196 return Ok(plan);
4197 }
4198
4199 Err(format!(
4204 "Unsupported trim termination combination: left={:?} right={:?}",
4205 left_side, right_side
4206 ))
4207}
4208
4209pub(crate) async fn execute_trim_operations_simple(
4221 strategy: Vec<TrimOperation>,
4222 current_scene_graph_delta: &crate::frontend::api::SceneGraphDelta,
4223 frontend: &mut crate::frontend::FrontendState,
4224 ctx: &crate::ExecutorContext,
4225 version: crate::frontend::api::Version,
4226 sketch_id: ObjectId,
4227) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
4228 use crate::frontend::SketchApi;
4229 use crate::frontend::sketch::Constraint;
4230 use crate::frontend::sketch::ExistingSegmentCtor;
4231 use crate::frontend::sketch::SegmentCtor;
4232
4233 let default_unit = frontend.default_length_unit();
4234
4235 let mut op_index = 0;
4236 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = None;
4237 let mut invalidates_ids = false;
4238
4239 while op_index < strategy.len() {
4240 let mut consumed_ops = 1;
4241 let operation_result = match &strategy[op_index] {
4242 TrimOperation::SimpleTrim { segment_to_trim_id } => {
4243 frontend
4245 .delete_objects(
4246 ctx,
4247 version,
4248 sketch_id,
4249 Vec::new(), vec![*segment_to_trim_id], )
4252 .await
4253 .map_err(|e| format!("Failed to delete segment: {}", e.error.message()))
4254 }
4255 TrimOperation::EditSegment {
4256 segment_id,
4257 ctor,
4258 endpoint_changed,
4259 } => {
4260 if op_index + 1 < strategy.len() {
4263 if let TrimOperation::AddCoincidentConstraint {
4264 segment_id: coincident_seg_id,
4265 endpoint_changed: coincident_endpoint_changed,
4266 segment_or_point_to_make_coincident_to,
4267 intersecting_endpoint_point_id,
4268 } = &strategy[op_index + 1]
4269 {
4270 if segment_id == coincident_seg_id && endpoint_changed == coincident_endpoint_changed {
4271 let mut delete_constraint_ids: Vec<ObjectId> = Vec::new();
4273 consumed_ops = 2;
4274
4275 if op_index + 2 < strategy.len()
4276 && let TrimOperation::DeleteConstraints { constraint_ids } = &strategy[op_index + 2]
4277 {
4278 delete_constraint_ids = constraint_ids.to_vec();
4279 consumed_ops = 3;
4280 }
4281
4282 let segment_ctor = ctor.clone();
4284
4285 let edited_segment = current_scene_graph_delta
4287 .new_graph
4288 .objects
4289 .iter()
4290 .find(|obj| obj.id == *segment_id)
4291 .ok_or_else(|| format!("Failed to find segment {} for tail-cut batch", segment_id.0))?;
4292
4293 let endpoint_point_id = match &edited_segment.kind {
4294 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4295 crate::frontend::sketch::Segment::Line(line) => {
4296 if *endpoint_changed == EndpointChanged::Start {
4297 line.start
4298 } else {
4299 line.end
4300 }
4301 }
4302 crate::frontend::sketch::Segment::Arc(arc) => {
4303 if *endpoint_changed == EndpointChanged::Start {
4304 arc.start
4305 } else {
4306 arc.end
4307 }
4308 }
4309 _ => {
4310 return Err("Unsupported segment type for tail-cut batch".to_string());
4311 }
4312 },
4313 _ => {
4314 return Err("Edited object is not a segment (tail-cut batch)".to_string());
4315 }
4316 };
4317
4318 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
4319 vec![endpoint_point_id.into(), (*point_id).into()]
4320 } else {
4321 vec![
4322 endpoint_point_id.into(),
4323 (*segment_or_point_to_make_coincident_to).into(),
4324 ]
4325 };
4326
4327 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4328 segments: coincident_segments,
4329 });
4330
4331 let segment_to_edit = ExistingSegmentCtor {
4332 id: *segment_id,
4333 ctor: segment_ctor,
4334 };
4335
4336 frontend
4339 .batch_tail_cut_operations(
4340 ctx,
4341 version,
4342 sketch_id,
4343 vec![segment_to_edit],
4344 vec![constraint],
4345 delete_constraint_ids,
4346 )
4347 .await
4348 .map_err(|e| format!("Failed to batch tail-cut operations: {}", e.error.message()))
4349 } else {
4350 let segment_to_edit = ExistingSegmentCtor {
4352 id: *segment_id,
4353 ctor: ctor.clone(),
4354 };
4355
4356 frontend
4357 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4358 .await
4359 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4360 }
4361 } else {
4362 let segment_to_edit = ExistingSegmentCtor {
4364 id: *segment_id,
4365 ctor: ctor.clone(),
4366 };
4367
4368 frontend
4369 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4370 .await
4371 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4372 }
4373 } else {
4374 let segment_to_edit = ExistingSegmentCtor {
4376 id: *segment_id,
4377 ctor: ctor.clone(),
4378 };
4379
4380 frontend
4381 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4382 .await
4383 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4384 }
4385 }
4386 TrimOperation::AddCoincidentConstraint {
4387 segment_id,
4388 endpoint_changed,
4389 segment_or_point_to_make_coincident_to,
4390 intersecting_endpoint_point_id,
4391 } => {
4392 let edited_segment = current_scene_graph_delta
4394 .new_graph
4395 .objects
4396 .iter()
4397 .find(|obj| obj.id == *segment_id)
4398 .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
4399
4400 let new_segment_endpoint_point_id = match &edited_segment.kind {
4402 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4403 crate::frontend::sketch::Segment::Line(line) => {
4404 if *endpoint_changed == EndpointChanged::Start {
4405 line.start
4406 } else {
4407 line.end
4408 }
4409 }
4410 crate::frontend::sketch::Segment::Arc(arc) => {
4411 if *endpoint_changed == EndpointChanged::Start {
4412 arc.start
4413 } else {
4414 arc.end
4415 }
4416 }
4417 _ => {
4418 return Err("Unsupported segment type for addCoincidentConstraint".to_string());
4419 }
4420 },
4421 _ => {
4422 return Err("Edited object is not a segment".to_string());
4423 }
4424 };
4425
4426 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
4428 vec![new_segment_endpoint_point_id.into(), (*point_id).into()]
4429 } else {
4430 vec![
4431 new_segment_endpoint_point_id.into(),
4432 (*segment_or_point_to_make_coincident_to).into(),
4433 ]
4434 };
4435
4436 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4437 segments: coincident_segments,
4438 });
4439
4440 frontend
4441 .add_constraint(ctx, version, sketch_id, constraint)
4442 .await
4443 .map_err(|e| format!("Failed to add constraint: {}", e.error.message()))
4444 }
4445 TrimOperation::DeleteConstraints { constraint_ids } => {
4446 let constraint_object_ids: Vec<ObjectId> = constraint_ids.to_vec();
4448
4449 frontend
4450 .delete_objects(
4451 ctx,
4452 version,
4453 sketch_id,
4454 constraint_object_ids,
4455 Vec::new(), )
4457 .await
4458 .map_err(|e| format!("Failed to delete constraints: {}", e.error.message()))
4459 }
4460 TrimOperation::ReplaceCircleWithArc {
4461 circle_id,
4462 arc_start_coords,
4463 arc_end_coords,
4464 arc_start_termination,
4465 arc_end_termination,
4466 } => {
4467 let original_circle = current_scene_graph_delta
4469 .new_graph
4470 .objects
4471 .iter()
4472 .find(|obj| obj.id == *circle_id)
4473 .ok_or_else(|| format!("Failed to find original circle {}", circle_id.0))?;
4474
4475 let (original_circle_start_id, original_circle_center_id, circle_ctor) = match &original_circle.kind {
4476 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4477 crate::frontend::sketch::Segment::Circle(circle) => match &circle.ctor {
4478 SegmentCtor::Circle(circle_ctor) => (circle.start, circle.center, circle_ctor.clone()),
4479 _ => return Err("Circle does not have a Circle ctor".to_string()),
4480 },
4481 _ => return Err("Original segment is not a circle".to_string()),
4482 },
4483 _ => return Err("Original object is not a segment".to_string()),
4484 };
4485
4486 let units = match &circle_ctor.start.x {
4487 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4488 _ => crate::pretty::NumericSuffix::Mm,
4489 };
4490
4491 let coords_to_point_expr = |coords: Coords2d| crate::frontend::sketch::Point2d {
4492 x: crate::frontend::api::Expr::Var(unit_to_number(coords.x, default_unit, units)),
4493 y: crate::frontend::api::Expr::Var(unit_to_number(coords.y, default_unit, units)),
4494 };
4495
4496 let arc_ctor = SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4497 start: coords_to_point_expr(*arc_start_coords),
4498 end: coords_to_point_expr(*arc_end_coords),
4499 center: circle_ctor.center.clone(),
4500 construction: circle_ctor.construction,
4501 });
4502
4503 let (_add_source_delta, add_scene_graph_delta) = frontend
4504 .add_segment(ctx, version, sketch_id, arc_ctor, None)
4505 .await
4506 .map_err(|e| format!("Failed to add arc while replacing circle: {}", e.error.message()))?;
4507 invalidates_ids = invalidates_ids || add_scene_graph_delta.invalidates_ids;
4508
4509 let new_arc_id = *add_scene_graph_delta
4510 .new_objects
4511 .iter()
4512 .find(|&id| {
4513 add_scene_graph_delta
4514 .new_graph
4515 .objects
4516 .iter()
4517 .find(|o| o.id == *id)
4518 .is_some_and(|obj| {
4519 matches!(
4520 &obj.kind,
4521 crate::frontend::api::ObjectKind::Segment { segment }
4522 if matches!(segment, crate::frontend::sketch::Segment::Arc(_))
4523 )
4524 })
4525 })
4526 .ok_or_else(|| "Failed to find newly created arc segment".to_string())?;
4527
4528 let new_arc_obj = add_scene_graph_delta
4529 .new_graph
4530 .objects
4531 .iter()
4532 .find(|obj| obj.id == new_arc_id)
4533 .ok_or_else(|| format!("New arc segment not found {}", new_arc_id.0))?;
4534 let (new_arc_start_id, new_arc_end_id, new_arc_center_id) = match &new_arc_obj.kind {
4535 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4536 crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, arc.center),
4537 _ => return Err("New segment is not an arc".to_string()),
4538 },
4539 _ => return Err("New arc object is not a segment".to_string()),
4540 };
4541
4542 let constraint_segments_for =
4543 |arc_endpoint_id: ObjectId,
4544 term: &TrimTermination|
4545 -> Result<Vec<crate::frontend::sketch::ConstraintSegment>, String> {
4546 match term {
4547 TrimTermination::Intersection {
4548 intersecting_seg_id, ..
4549 } => Ok(vec![arc_endpoint_id.into(), (*intersecting_seg_id).into()]),
4550 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4551 other_segment_point_id,
4552 ..
4553 } => Ok(vec![arc_endpoint_id.into(), (*other_segment_point_id).into()]),
4554 TrimTermination::SegEndPoint { .. } => {
4555 Err("Circle replacement endpoint cannot terminate at seg endpoint".to_string())
4556 }
4557 }
4558 };
4559
4560 let start_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4561 segments: constraint_segments_for(new_arc_start_id, arc_start_termination)?,
4562 });
4563 let (_c1_source_delta, c1_scene_graph_delta) = frontend
4564 .add_constraint(ctx, version, sketch_id, start_constraint)
4565 .await
4566 .map_err(|e| format!("Failed to add start coincident on replaced arc: {}", e.error.message()))?;
4567 invalidates_ids = invalidates_ids || c1_scene_graph_delta.invalidates_ids;
4568
4569 let end_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4570 segments: constraint_segments_for(new_arc_end_id, arc_end_termination)?,
4571 });
4572 let (_c2_source_delta, c2_scene_graph_delta) = frontend
4573 .add_constraint(ctx, version, sketch_id, end_constraint)
4574 .await
4575 .map_err(|e| format!("Failed to add end coincident on replaced arc: {}", e.error.message()))?;
4576 invalidates_ids = invalidates_ids || c2_scene_graph_delta.invalidates_ids;
4577
4578 let mut termination_point_ids: Vec<ObjectId> = Vec::new();
4579 for term in [arc_start_termination, arc_end_termination] {
4580 if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4581 other_segment_point_id,
4582 ..
4583 } = term.as_ref()
4584 {
4585 termination_point_ids.push(*other_segment_point_id);
4586 }
4587 }
4588
4589 let rewrite_map = std::collections::HashMap::from([
4593 (*circle_id, new_arc_id),
4594 (original_circle_center_id, new_arc_center_id),
4595 (original_circle_start_id, new_arc_start_id),
4596 ]);
4597 let rewrite_ids: std::collections::HashSet<ObjectId> = rewrite_map.keys().copied().collect();
4598
4599 let mut migrated_constraints: Vec<Constraint> = Vec::new();
4600 for obj in ¤t_scene_graph_delta.new_graph.objects {
4601 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4602 continue;
4603 };
4604
4605 match constraint {
4606 Constraint::Coincident(coincident) => {
4607 if !constraint_segments_reference_any(&coincident.segments, &rewrite_ids) {
4608 continue;
4609 }
4610
4611 if coincident.contains_segment(*circle_id)
4615 && coincident
4616 .segment_ids()
4617 .filter(|id| *id != *circle_id)
4618 .any(|id| termination_point_ids.contains(&id))
4619 {
4620 continue;
4621 }
4622
4623 let Some(Constraint::Coincident(migrated_coincident)) =
4624 rewrite_constraint_with_map(constraint, &rewrite_map)
4625 else {
4626 continue;
4627 };
4628
4629 let migrated_ids: Vec<ObjectId> = migrated_coincident
4633 .segments
4634 .iter()
4635 .filter_map(|segment| match segment {
4636 crate::frontend::sketch::ConstraintSegment::Segment(id) => Some(*id),
4637 crate::frontend::sketch::ConstraintSegment::Origin(_) => None,
4638 })
4639 .collect();
4640 if migrated_ids.contains(&new_arc_id)
4641 && (migrated_ids.contains(&new_arc_start_id) || migrated_ids.contains(&new_arc_end_id))
4642 {
4643 continue;
4644 }
4645
4646 migrated_constraints.push(Constraint::Coincident(migrated_coincident));
4647 }
4648 Constraint::Distance(distance) => {
4649 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4650 continue;
4651 }
4652 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4653 migrated_constraints.push(migrated);
4654 }
4655 }
4656 Constraint::HorizontalDistance(distance) => {
4657 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4658 continue;
4659 }
4660 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4661 migrated_constraints.push(migrated);
4662 }
4663 }
4664 Constraint::VerticalDistance(distance) => {
4665 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4666 continue;
4667 }
4668 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4669 migrated_constraints.push(migrated);
4670 }
4671 }
4672 Constraint::Radius(radius) => {
4673 if radius.arc == *circle_id
4674 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4675 {
4676 migrated_constraints.push(migrated);
4677 }
4678 }
4679 Constraint::Diameter(diameter) => {
4680 if diameter.arc == *circle_id
4681 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4682 {
4683 migrated_constraints.push(migrated);
4684 }
4685 }
4686 Constraint::EqualRadius(equal_radius) => {
4687 if equal_radius.input.contains(circle_id)
4688 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4689 {
4690 migrated_constraints.push(migrated);
4691 }
4692 }
4693 Constraint::Tangent(tangent) => {
4694 if tangent.input.contains(circle_id)
4695 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4696 {
4697 migrated_constraints.push(migrated);
4698 }
4699 }
4700 _ => {}
4701 }
4702 }
4703
4704 for constraint in migrated_constraints {
4705 let (_source_delta, migrated_scene_graph_delta) = frontend
4706 .add_constraint(ctx, version, sketch_id, constraint)
4707 .await
4708 .map_err(|e| format!("Failed to migrate circle constraint to arc: {}", e.error.message()))?;
4709 invalidates_ids = invalidates_ids || migrated_scene_graph_delta.invalidates_ids;
4710 }
4711
4712 frontend
4713 .delete_objects(ctx, version, sketch_id, Vec::new(), vec![*circle_id])
4714 .await
4715 .map_err(|e| format!("Failed to delete circle after arc replacement: {}", e.error.message()))
4716 }
4717 TrimOperation::SplitSegment {
4718 segment_id,
4719 left_trim_coords,
4720 right_trim_coords,
4721 original_end_coords,
4722 left_side,
4723 right_side,
4724 constraints_to_migrate,
4725 constraints_to_delete,
4726 ..
4727 } => {
4728 let original_segment = current_scene_graph_delta
4733 .new_graph
4734 .objects
4735 .iter()
4736 .find(|obj| obj.id == *segment_id)
4737 .ok_or_else(|| format!("Failed to find original segment {}", segment_id.0))?;
4738
4739 let (original_segment_start_point_id, original_segment_end_point_id, original_segment_center_point_id) =
4741 match &original_segment.kind {
4742 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4743 crate::frontend::sketch::Segment::Line(line) => (Some(line.start), Some(line.end), None),
4744 crate::frontend::sketch::Segment::Arc(arc) => {
4745 (Some(arc.start), Some(arc.end), Some(arc.center))
4746 }
4747 _ => (None, None, None),
4748 },
4749 _ => (None, None, None),
4750 };
4751
4752 let mut center_point_constraints_to_migrate: Vec<(Constraint, ObjectId)> = Vec::new();
4754 if let Some(original_center_id) = original_segment_center_point_id {
4755 for obj in ¤t_scene_graph_delta.new_graph.objects {
4756 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4757 continue;
4758 };
4759
4760 if let Constraint::Coincident(coincident) = constraint
4762 && coincident.contains_segment(original_center_id)
4763 {
4764 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
4765 }
4766
4767 if let Constraint::Distance(distance) = constraint
4769 && distance.contains_point(original_center_id)
4770 {
4771 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
4772 }
4773 }
4774 }
4775
4776 let (_segment_type, original_ctor) = match &original_segment.kind {
4778 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4779 crate::frontend::sketch::Segment::Line(line) => ("Line", line.ctor.clone()),
4780 crate::frontend::sketch::Segment::Arc(arc) => ("Arc", arc.ctor.clone()),
4781 _ => {
4782 return Err("Original segment is not a Line or Arc".to_string());
4783 }
4784 },
4785 _ => {
4786 return Err("Original object is not a segment".to_string());
4787 }
4788 };
4789
4790 let units = match &original_ctor {
4792 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
4793 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4794 _ => crate::pretty::NumericSuffix::Mm,
4795 },
4796 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
4797 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4798 _ => crate::pretty::NumericSuffix::Mm,
4799 },
4800 _ => crate::pretty::NumericSuffix::Mm,
4801 };
4802
4803 let coords_to_point =
4806 |coords: Coords2d| -> crate::frontend::sketch::Point2d<crate::frontend::api::Number> {
4807 crate::frontend::sketch::Point2d {
4808 x: unit_to_number(coords.x, default_unit, units),
4809 y: unit_to_number(coords.y, default_unit, units),
4810 }
4811 };
4812
4813 let point_to_expr = |point: crate::frontend::sketch::Point2d<crate::frontend::api::Number>| -> crate::frontend::sketch::Point2d<crate::frontend::api::Expr> {
4815 crate::frontend::sketch::Point2d {
4816 x: crate::frontend::api::Expr::Var(point.x),
4817 y: crate::frontend::api::Expr::Var(point.y),
4818 }
4819 };
4820
4821 let new_segment_ctor = match &original_ctor {
4823 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
4824 start: point_to_expr(coords_to_point(*right_trim_coords)),
4825 end: point_to_expr(coords_to_point(*original_end_coords)),
4826 construction: line_ctor.construction,
4827 }),
4828 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4829 start: point_to_expr(coords_to_point(*right_trim_coords)),
4830 end: point_to_expr(coords_to_point(*original_end_coords)),
4831 center: arc_ctor.center.clone(),
4832 construction: arc_ctor.construction,
4833 }),
4834 _ => {
4835 return Err("Unsupported segment type for new segment".to_string());
4836 }
4837 };
4838
4839 let (_add_source_delta, add_scene_graph_delta) = frontend
4840 .add_segment(ctx, version, sketch_id, new_segment_ctor, None)
4841 .await
4842 .map_err(|e| format!("Failed to add new segment: {}", e.error.message()))?;
4843
4844 let new_segment_id = *add_scene_graph_delta
4846 .new_objects
4847 .iter()
4848 .find(|&id| {
4849 if let Some(obj) = add_scene_graph_delta.new_graph.objects.iter().find(|o| o.id == *id) {
4850 matches!(
4851 &obj.kind,
4852 crate::frontend::api::ObjectKind::Segment { segment }
4853 if matches!(segment, crate::frontend::sketch::Segment::Line(_) | crate::frontend::sketch::Segment::Arc(_))
4854 )
4855 } else {
4856 false
4857 }
4858 })
4859 .ok_or_else(|| "Failed to find newly created segment".to_string())?;
4860
4861 let new_segment = add_scene_graph_delta
4862 .new_graph
4863 .objects
4864 .iter()
4865 .find(|o| o.id == new_segment_id)
4866 .ok_or_else(|| format!("New segment not found with id {}", new_segment_id.0))?;
4867
4868 let (new_segment_start_point_id, new_segment_end_point_id, new_segment_center_point_id) =
4870 match &new_segment.kind {
4871 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4872 crate::frontend::sketch::Segment::Line(line) => (line.start, line.end, None),
4873 crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, Some(arc.center)),
4874 _ => {
4875 return Err("New segment is not a Line or Arc".to_string());
4876 }
4877 },
4878 _ => {
4879 return Err("New segment is not a segment".to_string());
4880 }
4881 };
4882
4883 let edited_ctor = match &original_ctor {
4885 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
4886 start: line_ctor.start.clone(),
4887 end: point_to_expr(coords_to_point(*left_trim_coords)),
4888 construction: line_ctor.construction,
4889 }),
4890 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4891 start: arc_ctor.start.clone(),
4892 end: point_to_expr(coords_to_point(*left_trim_coords)),
4893 center: arc_ctor.center.clone(),
4894 construction: arc_ctor.construction,
4895 }),
4896 _ => {
4897 return Err("Unsupported segment type for split".to_string());
4898 }
4899 };
4900
4901 let (_edit_source_delta, edit_scene_graph_delta) = frontend
4902 .edit_segments(
4903 ctx,
4904 version,
4905 sketch_id,
4906 vec![ExistingSegmentCtor {
4907 id: *segment_id,
4908 ctor: edited_ctor,
4909 }],
4910 )
4911 .await
4912 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))?;
4913 invalidates_ids = invalidates_ids || edit_scene_graph_delta.invalidates_ids;
4915
4916 let edited_segment = edit_scene_graph_delta
4918 .new_graph
4919 .objects
4920 .iter()
4921 .find(|obj| obj.id == *segment_id)
4922 .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
4923
4924 let left_side_endpoint_point_id = match &edited_segment.kind {
4925 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4926 crate::frontend::sketch::Segment::Line(line) => line.end,
4927 crate::frontend::sketch::Segment::Arc(arc) => arc.end,
4928 _ => {
4929 return Err("Edited segment is not a Line or Arc".to_string());
4930 }
4931 },
4932 _ => {
4933 return Err("Edited segment is not a segment".to_string());
4934 }
4935 };
4936
4937 let mut batch_constraints = Vec::new();
4939
4940 let left_intersecting_seg_id = match &**left_side {
4942 TrimTermination::Intersection {
4943 intersecting_seg_id, ..
4944 }
4945 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4946 intersecting_seg_id, ..
4947 } => *intersecting_seg_id,
4948 _ => {
4949 return Err("Left side is not an intersection or coincident".to_string());
4950 }
4951 };
4952 let left_coincident_segments = match &**left_side {
4953 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4954 other_segment_point_id,
4955 ..
4956 } => {
4957 vec![left_side_endpoint_point_id.into(), (*other_segment_point_id).into()]
4958 }
4959 _ => {
4960 vec![left_side_endpoint_point_id.into(), left_intersecting_seg_id.into()]
4961 }
4962 };
4963 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
4964 segments: left_coincident_segments,
4965 }));
4966
4967 let right_intersecting_seg_id = match &**right_side {
4969 TrimTermination::Intersection {
4970 intersecting_seg_id, ..
4971 }
4972 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4973 intersecting_seg_id, ..
4974 } => *intersecting_seg_id,
4975 _ => {
4976 return Err("Right side is not an intersection or coincident".to_string());
4977 }
4978 };
4979
4980 let mut intersection_point_id: Option<ObjectId> = None;
4981 if matches!(&**right_side, TrimTermination::Intersection { .. }) {
4982 let intersecting_seg = edit_scene_graph_delta
4983 .new_graph
4984 .objects
4985 .iter()
4986 .find(|obj| obj.id == right_intersecting_seg_id);
4987
4988 if let Some(seg) = intersecting_seg {
4989 let endpoint_epsilon = 1e-3; let right_trim_coords_value = *right_trim_coords;
4991
4992 if let crate::frontend::api::ObjectKind::Segment { segment } = &seg.kind {
4993 match segment {
4994 crate::frontend::sketch::Segment::Line(_) => {
4995 if let (Some(start_coords), Some(end_coords)) = (
4996 crate::frontend::trim::get_position_coords_for_line(
4997 seg,
4998 crate::frontend::trim::LineEndpoint::Start,
4999 &edit_scene_graph_delta.new_graph.objects,
5000 default_unit,
5001 ),
5002 crate::frontend::trim::get_position_coords_for_line(
5003 seg,
5004 crate::frontend::trim::LineEndpoint::End,
5005 &edit_scene_graph_delta.new_graph.objects,
5006 default_unit,
5007 ),
5008 ) {
5009 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
5010 * (right_trim_coords_value.x - start_coords.x)
5011 + (right_trim_coords_value.y - start_coords.y)
5012 * (right_trim_coords_value.y - start_coords.y))
5013 .sqrt();
5014 if dist_to_start < endpoint_epsilon {
5015 if let crate::frontend::sketch::Segment::Line(line) = segment {
5016 intersection_point_id = Some(line.start);
5017 }
5018 } else {
5019 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
5020 * (right_trim_coords_value.x - end_coords.x)
5021 + (right_trim_coords_value.y - end_coords.y)
5022 * (right_trim_coords_value.y - end_coords.y))
5023 .sqrt();
5024 if dist_to_end < endpoint_epsilon
5025 && let crate::frontend::sketch::Segment::Line(line) = segment
5026 {
5027 intersection_point_id = Some(line.end);
5028 }
5029 }
5030 }
5031 }
5032 crate::frontend::sketch::Segment::Arc(_) => {
5033 if let (Some(start_coords), Some(end_coords)) = (
5034 crate::frontend::trim::get_position_coords_from_arc(
5035 seg,
5036 crate::frontend::trim::ArcPoint::Start,
5037 &edit_scene_graph_delta.new_graph.objects,
5038 default_unit,
5039 ),
5040 crate::frontend::trim::get_position_coords_from_arc(
5041 seg,
5042 crate::frontend::trim::ArcPoint::End,
5043 &edit_scene_graph_delta.new_graph.objects,
5044 default_unit,
5045 ),
5046 ) {
5047 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
5048 * (right_trim_coords_value.x - start_coords.x)
5049 + (right_trim_coords_value.y - start_coords.y)
5050 * (right_trim_coords_value.y - start_coords.y))
5051 .sqrt();
5052 if dist_to_start < endpoint_epsilon {
5053 if let crate::frontend::sketch::Segment::Arc(arc) = segment {
5054 intersection_point_id = Some(arc.start);
5055 }
5056 } else {
5057 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
5058 * (right_trim_coords_value.x - end_coords.x)
5059 + (right_trim_coords_value.y - end_coords.y)
5060 * (right_trim_coords_value.y - end_coords.y))
5061 .sqrt();
5062 if dist_to_end < endpoint_epsilon
5063 && let crate::frontend::sketch::Segment::Arc(arc) = segment
5064 {
5065 intersection_point_id = Some(arc.end);
5066 }
5067 }
5068 }
5069 }
5070 _ => {}
5071 }
5072 }
5073 }
5074 }
5075
5076 let right_coincident_segments = if let Some(point_id) = intersection_point_id {
5077 vec![new_segment_start_point_id.into(), point_id.into()]
5078 } else if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5079 other_segment_point_id,
5080 ..
5081 } = &**right_side
5082 {
5083 vec![new_segment_start_point_id.into(), (*other_segment_point_id).into()]
5084 } else {
5085 vec![new_segment_start_point_id.into(), right_intersecting_seg_id.into()]
5086 };
5087 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
5088 segments: right_coincident_segments,
5089 }));
5090
5091 let mut points_constrained_to_new_segment_start = std::collections::HashSet::new();
5093 let mut points_constrained_to_new_segment_end = std::collections::HashSet::new();
5094
5095 if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5096 other_segment_point_id,
5097 ..
5098 } = &**right_side
5099 {
5100 points_constrained_to_new_segment_start.insert(other_segment_point_id);
5101 }
5102
5103 for constraint_to_migrate in constraints_to_migrate.iter() {
5104 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::End
5105 && constraint_to_migrate.is_point_point
5106 {
5107 points_constrained_to_new_segment_end.insert(constraint_to_migrate.other_entity_id);
5108 }
5109 }
5110
5111 for constraint_to_migrate in constraints_to_migrate.iter() {
5112 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment
5114 && (points_constrained_to_new_segment_start.contains(&constraint_to_migrate.other_entity_id)
5115 || points_constrained_to_new_segment_end.contains(&constraint_to_migrate.other_entity_id))
5116 {
5117 continue; }
5119
5120 let constraint_segments = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment {
5121 vec![constraint_to_migrate.other_entity_id.into(), new_segment_id.into()]
5122 } else {
5123 let target_endpoint_id = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Start
5124 {
5125 new_segment_start_point_id
5126 } else {
5127 new_segment_end_point_id
5128 };
5129 vec![target_endpoint_id.into(), constraint_to_migrate.other_entity_id.into()]
5130 };
5131 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
5132 segments: constraint_segments,
5133 }));
5134 }
5135
5136 let mut distance_constraints_to_re_add: Vec<(
5138 crate::frontend::api::Number,
5139 crate::frontend::sketch::ConstraintSource,
5140 )> = Vec::new();
5141 if let (Some(original_start_id), Some(original_end_id)) =
5142 (original_segment_start_point_id, original_segment_end_point_id)
5143 {
5144 for obj in &edit_scene_graph_delta.new_graph.objects {
5145 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5146 continue;
5147 };
5148
5149 let Constraint::Distance(distance) = constraint else {
5150 continue;
5151 };
5152
5153 let references_start = distance.contains_point(original_start_id);
5154 let references_end = distance.contains_point(original_end_id);
5155
5156 if references_start && references_end {
5157 distance_constraints_to_re_add.push((distance.distance, distance.source.clone()));
5158 }
5159 }
5160 }
5161
5162 if let Some(original_start_id) = original_segment_start_point_id {
5164 for (distance_value, source) in distance_constraints_to_re_add {
5165 batch_constraints.push(Constraint::Distance(crate::frontend::sketch::Distance {
5166 points: vec![original_start_id.into(), new_segment_end_point_id.into()],
5167 distance: distance_value,
5168 source,
5169 }));
5170 }
5171 }
5172
5173 if let Some(new_center_id) = new_segment_center_point_id {
5175 for (constraint, original_center_id) in center_point_constraints_to_migrate {
5176 let center_rewrite_map = std::collections::HashMap::from([(original_center_id, new_center_id)]);
5177 if let Some(rewritten) = rewrite_constraint_with_map(&constraint, ¢er_rewrite_map)
5178 && matches!(rewritten, Constraint::Coincident(_) | Constraint::Distance(_))
5179 {
5180 batch_constraints.push(rewritten);
5181 }
5182 }
5183 }
5184
5185 let mut angle_rewrite_map = std::collections::HashMap::from([(*segment_id, new_segment_id)]);
5187 if let Some(original_end_id) = original_segment_end_point_id {
5188 angle_rewrite_map.insert(original_end_id, new_segment_end_point_id);
5189 }
5190 for obj in &edit_scene_graph_delta.new_graph.objects {
5191 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5192 continue;
5193 };
5194
5195 let should_migrate = match constraint {
5196 Constraint::Parallel(parallel) => parallel.lines.contains(segment_id),
5197 Constraint::Perpendicular(perpendicular) => perpendicular.lines.contains(segment_id),
5198 Constraint::Midpoint(midpoint) => {
5199 midpoint.segment == *segment_id
5200 || original_segment_end_point_id.is_some_and(|end_id| midpoint.point == end_id)
5201 }
5202 Constraint::Horizontal(Horizontal::Line { line }) => line == segment_id,
5203 Constraint::Horizontal(Horizontal::Points { points }) => original_segment_end_point_id
5204 .is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
5205 Constraint::Vertical(Vertical::Line { line }) => line == segment_id,
5206 Constraint::Vertical(Vertical::Points { points }) => original_segment_end_point_id
5207 .is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
5208 _ => false,
5209 };
5210
5211 if should_migrate
5212 && let Some(migrated_constraint) = rewrite_constraint_with_map(constraint, &angle_rewrite_map)
5213 && matches!(
5214 migrated_constraint,
5215 Constraint::Midpoint(_)
5216 | Constraint::Parallel(_)
5217 | Constraint::Perpendicular(_)
5218 | Constraint::Horizontal(_)
5219 | Constraint::Vertical(_)
5220 )
5221 {
5222 batch_constraints.push(migrated_constraint);
5223 }
5224 }
5225
5226 let constraint_object_ids: Vec<ObjectId> = constraints_to_delete.to_vec();
5228
5229 let batch_result = frontend
5230 .batch_split_segment_operations(
5231 ctx,
5232 version,
5233 sketch_id,
5234 Vec::new(), batch_constraints,
5236 constraint_object_ids,
5237 crate::frontend::sketch::NewSegmentInfo {
5238 segment_id: new_segment_id,
5239 start_point_id: new_segment_start_point_id,
5240 end_point_id: new_segment_end_point_id,
5241 center_point_id: new_segment_center_point_id,
5242 },
5243 )
5244 .await
5245 .map_err(|e| format!("Failed to batch split segment operations: {}", e.error.message()));
5246 if let Ok((_, ref batch_delta)) = batch_result {
5248 invalidates_ids = invalidates_ids || batch_delta.invalidates_ids;
5249 }
5250 batch_result
5251 }
5252 };
5253
5254 match operation_result {
5255 Ok((source_delta, scene_graph_delta)) => {
5256 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
5258 last_result = Some((source_delta, scene_graph_delta.clone()));
5259 }
5260 Err(e) => {
5261 crate::logln!("Error executing trim operation {}: {}", op_index, e);
5262 }
5264 }
5265
5266 op_index += consumed_ops;
5267 }
5268
5269 let (source_delta, mut scene_graph_delta) =
5270 last_result.ok_or_else(|| "No operations were executed successfully".to_string())?;
5271 scene_graph_delta.invalidates_ids = invalidates_ids;
5273 Ok((source_delta, scene_graph_delta))
5274}