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 label_position: distance.label_position.clone(),
350 source: distance.source.clone(),
351 })),
352 Constraint::HorizontalDistance(distance) => {
353 Some(Constraint::HorizontalDistance(crate::frontend::sketch::Distance {
354 points: rewrite_constraint_segments(&distance.points, rewrite_map),
355 distance: distance.distance,
356 label_position: distance.label_position.clone(),
357 source: distance.source.clone(),
358 }))
359 }
360 Constraint::VerticalDistance(distance) => {
361 Some(Constraint::VerticalDistance(crate::frontend::sketch::Distance {
362 points: rewrite_constraint_segments(&distance.points, rewrite_map),
363 distance: distance.distance,
364 label_position: distance.label_position.clone(),
365 source: distance.source.clone(),
366 }))
367 }
368 Constraint::Radius(radius) => Some(Constraint::Radius(crate::frontend::sketch::Radius {
369 arc: rewrite_object_id(radius.arc, rewrite_map),
370 radius: radius.radius,
371 label_position: radius.label_position.clone(),
372 source: radius.source.clone(),
373 })),
374 Constraint::Diameter(diameter) => Some(Constraint::Diameter(crate::frontend::sketch::Diameter {
375 arc: rewrite_object_id(diameter.arc, rewrite_map),
376 diameter: diameter.diameter,
377 label_position: diameter.label_position.clone(),
378 source: diameter.source.clone(),
379 })),
380 Constraint::EqualRadius(equal_radius) => Some(Constraint::EqualRadius(crate::frontend::sketch::EqualRadius {
381 input: equal_radius
382 .input
383 .iter()
384 .map(|id| rewrite_object_id(*id, rewrite_map))
385 .collect(),
386 })),
387 Constraint::Midpoint(midpoint) => Some(Constraint::Midpoint(crate::frontend::sketch::Midpoint {
388 point: rewrite_object_id(midpoint.point, rewrite_map),
389 segment: rewrite_object_id(midpoint.segment, rewrite_map),
390 })),
391 Constraint::Tangent(tangent) => Some(Constraint::Tangent(crate::frontend::sketch::Tangent {
392 input: tangent
393 .input
394 .iter()
395 .map(|id| rewrite_object_id(*id, rewrite_map))
396 .collect(),
397 })),
398 Constraint::Symmetric(symmetric) => Some(Constraint::Symmetric(crate::frontend::sketch::Symmetric {
399 input: symmetric
400 .input
401 .iter()
402 .map(|id| rewrite_object_id(*id, rewrite_map))
403 .collect(),
404 axis: rewrite_object_id(symmetric.axis, rewrite_map),
405 })),
406 Constraint::Parallel(parallel) => Some(Constraint::Parallel(crate::frontend::sketch::Parallel {
407 lines: parallel
408 .lines
409 .iter()
410 .map(|id| rewrite_object_id(*id, rewrite_map))
411 .collect(),
412 })),
413 Constraint::Perpendicular(perpendicular) => {
414 Some(Constraint::Perpendicular(crate::frontend::sketch::Perpendicular {
415 lines: perpendicular
416 .lines
417 .iter()
418 .map(|id| rewrite_object_id(*id, rewrite_map))
419 .collect(),
420 }))
421 }
422 Constraint::Horizontal(horizontal) => match horizontal {
423 crate::front::Horizontal::Line { line } => {
424 Some(Constraint::Horizontal(crate::frontend::sketch::Horizontal::Line {
425 line: rewrite_object_id(*line, rewrite_map),
426 }))
427 }
428 crate::front::Horizontal::Points { points } => Some(Constraint::Horizontal(Horizontal::Points {
429 points: points
430 .iter()
431 .map(|point| match point {
432 crate::frontend::sketch::ConstraintSegment::Segment(point) => {
433 crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
434 }
435 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
436 crate::frontend::sketch::ConstraintSegment::Origin(*origin)
437 }
438 })
439 .collect(),
440 })),
441 },
442 Constraint::Vertical(vertical) => match vertical {
443 crate::front::Vertical::Line { line } => {
444 Some(Constraint::Vertical(crate::frontend::sketch::Vertical::Line {
445 line: rewrite_object_id(*line, rewrite_map),
446 }))
447 }
448 crate::front::Vertical::Points { points } => Some(Constraint::Vertical(Vertical::Points {
449 points: points
450 .iter()
451 .map(|point| match point {
452 crate::frontend::sketch::ConstraintSegment::Segment(point) => {
453 crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
454 }
455 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
456 crate::frontend::sketch::ConstraintSegment::Origin(*origin)
457 }
458 })
459 .collect(),
460 })),
461 },
462 _ => None,
463 }
464}
465
466fn point_axis_constraint_references_point(constraint: &Constraint, point_id: ObjectId) -> bool {
467 match constraint {
468 Constraint::Horizontal(Horizontal::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
469 Constraint::Vertical(Vertical::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
470 _ => false,
471 }
472}
473
474#[derive(Debug, Clone)]
475#[allow(clippy::large_enum_variant)]
476pub enum TrimOperation {
477 SimpleTrim {
478 segment_to_trim_id: ObjectId,
479 },
480 EditSegment {
481 segment_id: ObjectId,
482 ctor: SegmentCtor,
483 endpoint_changed: EndpointChanged,
484 },
485 AddCoincidentConstraint {
486 segment_id: ObjectId,
487 endpoint_changed: EndpointChanged,
488 segment_or_point_to_make_coincident_to: ObjectId,
489 intersecting_endpoint_point_id: Option<ObjectId>,
490 },
491 SplitSegment {
492 segment_id: ObjectId,
493 left_trim_coords: Coords2d,
494 right_trim_coords: Coords2d,
495 original_end_coords: Coords2d,
496 left_side: Box<TrimTermination>,
497 right_side: Box<TrimTermination>,
498 left_side_coincident_data: CoincidentData,
499 right_side_coincident_data: CoincidentData,
500 constraints_to_migrate: Vec<ConstraintToMigrate>,
501 constraints_to_delete: Vec<ObjectId>,
502 },
503 ReplaceCircleWithArc {
504 circle_id: ObjectId,
505 arc_start_coords: Coords2d,
506 arc_end_coords: Coords2d,
507 arc_start_termination: Box<TrimTermination>,
508 arc_end_termination: Box<TrimTermination>,
509 },
510 DeleteConstraints {
511 constraint_ids: Vec<ObjectId>,
512 },
513}
514
515pub fn is_point_on_line_segment(
519 point: Coords2d,
520 segment_start: Coords2d,
521 segment_end: Coords2d,
522 epsilon: f64,
523) -> Option<Coords2d> {
524 let dx = segment_end.x - segment_start.x;
525 let dy = segment_end.y - segment_start.y;
526 let segment_length_sq = dx * dx + dy * dy;
527
528 if segment_length_sq < EPSILON_PARALLEL {
529 let dist_sq = (point.x - segment_start.x) * (point.x - segment_start.x)
531 + (point.y - segment_start.y) * (point.y - segment_start.y);
532 if dist_sq <= epsilon * epsilon {
533 return Some(point);
534 }
535 return None;
536 }
537
538 let point_dx = point.x - segment_start.x;
539 let point_dy = point.y - segment_start.y;
540 let projection_param = (point_dx * dx + point_dy * dy) / segment_length_sq;
541
542 if !(0.0..=1.0).contains(&projection_param) {
544 return None;
545 }
546
547 let projected_point = Coords2d {
549 x: segment_start.x + projection_param * dx,
550 y: segment_start.y + projection_param * dy,
551 };
552
553 let dist_dx = point.x - projected_point.x;
555 let dist_dy = point.y - projected_point.y;
556 let distance_sq = dist_dx * dist_dx + dist_dy * dist_dy;
557
558 if distance_sq <= epsilon * epsilon {
559 Some(point)
560 } else {
561 None
562 }
563}
564
565pub fn line_segment_intersection(
569 line1_start: Coords2d,
570 line1_end: Coords2d,
571 line2_start: Coords2d,
572 line2_end: Coords2d,
573 epsilon: f64,
574) -> Option<Coords2d> {
575 if let Some(point) = is_point_on_line_segment(line1_start, line2_start, line2_end, epsilon) {
577 return Some(point);
578 }
579
580 if let Some(point) = is_point_on_line_segment(line1_end, line2_start, line2_end, epsilon) {
581 return Some(point);
582 }
583
584 if let Some(point) = is_point_on_line_segment(line2_start, line1_start, line1_end, epsilon) {
585 return Some(point);
586 }
587
588 if let Some(point) = is_point_on_line_segment(line2_end, line1_start, line1_end, epsilon) {
589 return Some(point);
590 }
591
592 let x1 = line1_start.x;
594 let y1 = line1_start.y;
595 let x2 = line1_end.x;
596 let y2 = line1_end.y;
597 let x3 = line2_start.x;
598 let y3 = line2_start.y;
599 let x4 = line2_end.x;
600 let y4 = line2_end.y;
601
602 let denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
603 if denominator.abs() < EPSILON_PARALLEL {
604 return None;
606 }
607
608 let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denominator;
609 let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denominator;
610
611 if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
613 let x = x1 + t * (x2 - x1);
614 let y = y1 + t * (y2 - y1);
615 return Some(Coords2d { x, y });
616 }
617
618 None
619}
620
621pub fn project_point_onto_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
626 let dx = segment_end.x - segment_start.x;
627 let dy = segment_end.y - segment_start.y;
628 let segment_length_sq = dx * dx + dy * dy;
629
630 if segment_length_sq < EPSILON_PARALLEL {
631 return 0.0;
633 }
634
635 let point_dx = point.x - segment_start.x;
636 let point_dy = point.y - segment_start.y;
637
638 (point_dx * dx + point_dy * dy) / segment_length_sq
639}
640
641pub fn perpendicular_distance_to_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
645 let dx = segment_end.x - segment_start.x;
646 let dy = segment_end.y - segment_start.y;
647 let segment_length_sq = dx * dx + dy * dy;
648
649 if segment_length_sq < EPSILON_PARALLEL {
650 let dist_dx = point.x - segment_start.x;
652 let dist_dy = point.y - segment_start.y;
653 return (dist_dx * dist_dx + dist_dy * dist_dy).sqrt();
654 }
655
656 let point_dx = point.x - segment_start.x;
658 let point_dy = point.y - segment_start.y;
659
660 let t = (point_dx * dx + point_dy * dy) / segment_length_sq;
662
663 let clamped_t = t.clamp(0.0, 1.0);
665 let closest_point = Coords2d {
666 x: segment_start.x + clamped_t * dx,
667 y: segment_start.y + clamped_t * dy,
668 };
669
670 let dist_dx = point.x - closest_point.x;
672 let dist_dy = point.y - closest_point.y;
673 (dist_dx * dist_dx + dist_dy * dist_dy).sqrt()
674}
675
676pub fn is_point_on_arc(point: Coords2d, center: Coords2d, start: Coords2d, end: Coords2d, epsilon: f64) -> bool {
680 let radius = ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
682
683 let dist_from_center =
685 ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
686 if (dist_from_center - radius).abs() > epsilon {
687 return false;
688 }
689
690 let start_angle = libm::atan2(start.y - center.y, start.x - center.x);
692 let end_angle = libm::atan2(end.y - center.y, end.x - center.x);
693 let point_angle = libm::atan2(point.y - center.y, point.x - center.x);
694
695 let normalize_angle = |angle: f64| -> f64 {
697 if !angle.is_finite() {
698 return angle;
699 }
700 let mut normalized = angle;
701 while normalized < 0.0 {
702 normalized += TAU;
703 }
704 while normalized >= TAU {
705 normalized -= TAU;
706 }
707 normalized
708 };
709
710 let normalized_start = normalize_angle(start_angle);
711 let normalized_end = normalize_angle(end_angle);
712 let normalized_point = normalize_angle(point_angle);
713
714 if normalized_start < normalized_end {
718 normalized_point >= normalized_start && normalized_point <= normalized_end
720 } else {
721 normalized_point >= normalized_start || normalized_point <= normalized_end
723 }
724}
725
726pub fn line_arc_intersection(
730 line_start: Coords2d,
731 line_end: Coords2d,
732 arc_center: Coords2d,
733 arc_start: Coords2d,
734 arc_end: Coords2d,
735 epsilon: f64,
736) -> Option<Coords2d> {
737 let radius = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
739 + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
740 .sqrt();
741
742 let translated_line_start = Coords2d {
744 x: line_start.x - arc_center.x,
745 y: line_start.y - arc_center.y,
746 };
747 let translated_line_end = Coords2d {
748 x: line_end.x - arc_center.x,
749 y: line_end.y - arc_center.y,
750 };
751
752 let dx = translated_line_end.x - translated_line_start.x;
754 let dy = translated_line_end.y - translated_line_start.y;
755
756 let a = dx * dx + dy * dy;
763 let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
764 let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
765 - radius * radius;
766
767 let discriminant = b * b - 4.0 * a * c;
768
769 if discriminant < 0.0 {
770 return None;
772 }
773
774 if a.abs() < EPSILON_PARALLEL {
775 let dist_from_center = (translated_line_start.x * translated_line_start.x
777 + translated_line_start.y * translated_line_start.y)
778 .sqrt();
779 if (dist_from_center - radius).abs() <= epsilon {
780 let point = line_start;
782 if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
783 return Some(point);
784 }
785 }
786 return None;
787 }
788
789 let sqrt_discriminant = discriminant.sqrt();
790 let t1 = (-b - sqrt_discriminant) / (2.0 * a);
791 let t2 = (-b + sqrt_discriminant) / (2.0 * a);
792
793 let mut candidates: Vec<(f64, Coords2d)> = Vec::new();
795 if (0.0..=1.0).contains(&t1) {
796 let point = Coords2d {
797 x: line_start.x + t1 * (line_end.x - line_start.x),
798 y: line_start.y + t1 * (line_end.y - line_start.y),
799 };
800 candidates.push((t1, point));
801 }
802 if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
803 let point = Coords2d {
804 x: line_start.x + t2 * (line_end.x - line_start.x),
805 y: line_start.y + t2 * (line_end.y - line_start.y),
806 };
807 candidates.push((t2, point));
808 }
809
810 for (_t, point) in candidates {
812 if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
813 return Some(point);
814 }
815 }
816
817 None
818}
819
820pub fn line_circle_intersections(
825 line_start: Coords2d,
826 line_end: Coords2d,
827 circle_center: Coords2d,
828 radius: f64,
829 epsilon: f64,
830) -> Vec<(f64, Coords2d)> {
831 let translated_line_start = Coords2d {
833 x: line_start.x - circle_center.x,
834 y: line_start.y - circle_center.y,
835 };
836 let translated_line_end = Coords2d {
837 x: line_end.x - circle_center.x,
838 y: line_end.y - circle_center.y,
839 };
840
841 let dx = translated_line_end.x - translated_line_start.x;
842 let dy = translated_line_end.y - translated_line_start.y;
843 let a = dx * dx + dy * dy;
844 let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
845 let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
846 - radius * radius;
847
848 if a.abs() < EPSILON_PARALLEL {
849 return Vec::new();
850 }
851
852 let discriminant = b * b - 4.0 * a * c;
853 if discriminant < 0.0 {
854 return Vec::new();
855 }
856
857 let sqrt_discriminant = discriminant.sqrt();
858 let mut intersections = Vec::new();
859
860 let t1 = (-b - sqrt_discriminant) / (2.0 * a);
861 if (0.0..=1.0).contains(&t1) {
862 intersections.push((
863 t1,
864 Coords2d {
865 x: line_start.x + t1 * (line_end.x - line_start.x),
866 y: line_start.y + t1 * (line_end.y - line_start.y),
867 },
868 ));
869 }
870
871 let t2 = (-b + sqrt_discriminant) / (2.0 * a);
872 if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
873 intersections.push((
874 t2,
875 Coords2d {
876 x: line_start.x + t2 * (line_end.x - line_start.x),
877 y: line_start.y + t2 * (line_end.y - line_start.y),
878 },
879 ));
880 }
881
882 intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
883 intersections
884}
885
886pub fn project_point_onto_circle(point: Coords2d, center: Coords2d, start: Coords2d) -> f64 {
892 let normalize_angle = |angle: f64| -> f64 {
893 if !angle.is_finite() {
894 return angle;
895 }
896 let mut normalized = angle;
897 while normalized < 0.0 {
898 normalized += TAU;
899 }
900 while normalized >= TAU {
901 normalized -= TAU;
902 }
903 normalized
904 };
905
906 let start_angle = normalize_angle(libm::atan2(start.y - center.y, start.x - center.x));
907 let point_angle = normalize_angle(libm::atan2(point.y - center.y, point.x - center.x));
908 let delta_ccw = (point_angle - start_angle).rem_euclid(TAU);
909 delta_ccw / TAU
910}
911
912fn is_point_on_circle(point: Coords2d, center: Coords2d, radius: f64, epsilon: f64) -> bool {
913 let dist = ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
914 (dist - radius).abs() <= epsilon
915}
916
917pub fn project_point_onto_arc(point: Coords2d, arc_center: Coords2d, arc_start: Coords2d, arc_end: Coords2d) -> f64 {
920 let start_angle = libm::atan2(arc_start.y - arc_center.y, arc_start.x - arc_center.x);
922 let end_angle = libm::atan2(arc_end.y - arc_center.y, arc_end.x - arc_center.x);
923 let point_angle = libm::atan2(point.y - arc_center.y, point.x - arc_center.x);
924
925 let normalize_angle = |angle: f64| -> f64 {
927 if !angle.is_finite() {
928 return angle;
929 }
930 let mut normalized = angle;
931 while normalized < 0.0 {
932 normalized += TAU;
933 }
934 while normalized >= TAU {
935 normalized -= TAU;
936 }
937 normalized
938 };
939
940 let normalized_start = normalize_angle(start_angle);
941 let normalized_end = normalize_angle(end_angle);
942 let normalized_point = normalize_angle(point_angle);
943
944 let arc_length = if normalized_start < normalized_end {
946 normalized_end - normalized_start
947 } else {
948 TAU - normalized_start + normalized_end
950 };
951
952 if arc_length < EPSILON_PARALLEL {
953 return 0.0;
955 }
956
957 let point_arc_length = if normalized_start < normalized_end {
959 if normalized_point >= normalized_start && normalized_point <= normalized_end {
960 normalized_point - normalized_start
961 } else {
962 let dist_to_start = libm::fmin(
964 (normalized_point - normalized_start).abs(),
965 TAU - (normalized_point - normalized_start).abs(),
966 );
967 let dist_to_end = libm::fmin(
968 (normalized_point - normalized_end).abs(),
969 TAU - (normalized_point - normalized_end).abs(),
970 );
971 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
972 }
973 } else {
974 if normalized_point >= normalized_start || normalized_point <= normalized_end {
976 if normalized_point >= normalized_start {
977 normalized_point - normalized_start
978 } else {
979 TAU - normalized_start + normalized_point
980 }
981 } else {
982 let dist_to_start = libm::fmin(
984 (normalized_point - normalized_start).abs(),
985 TAU - (normalized_point - normalized_start).abs(),
986 );
987 let dist_to_end = libm::fmin(
988 (normalized_point - normalized_end).abs(),
989 TAU - (normalized_point - normalized_end).abs(),
990 );
991 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
992 }
993 };
994
995 point_arc_length / arc_length
997}
998
999pub fn arc_arc_intersections(
1003 arc1_center: Coords2d,
1004 arc1_start: Coords2d,
1005 arc1_end: Coords2d,
1006 arc2_center: Coords2d,
1007 arc2_start: Coords2d,
1008 arc2_end: Coords2d,
1009 epsilon: f64,
1010) -> Vec<Coords2d> {
1011 let r1 = ((arc1_start.x - arc1_center.x) * (arc1_start.x - arc1_center.x)
1013 + (arc1_start.y - arc1_center.y) * (arc1_start.y - arc1_center.y))
1014 .sqrt();
1015 let r2 = ((arc2_start.x - arc2_center.x) * (arc2_start.x - arc2_center.x)
1016 + (arc2_start.y - arc2_center.y) * (arc2_start.y - arc2_center.y))
1017 .sqrt();
1018
1019 let dx = arc2_center.x - arc1_center.x;
1021 let dy = arc2_center.y - arc1_center.y;
1022 let d = (dx * dx + dy * dy).sqrt();
1023
1024 if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon {
1026 return Vec::new();
1028 }
1029
1030 if d < EPSILON_PARALLEL {
1032 return Vec::new();
1034 }
1035
1036 let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1039 let h_sq = r1 * r1 - a * a;
1040
1041 if h_sq < 0.0 {
1043 return Vec::new();
1044 }
1045
1046 let h = h_sq.sqrt();
1047
1048 if h.is_nan() {
1050 return Vec::new();
1051 }
1052
1053 let ux = dx / d;
1055 let uy = dy / d;
1056
1057 let px = -uy;
1059 let py = ux;
1060
1061 let mid_point = Coords2d {
1063 x: arc1_center.x + a * ux,
1064 y: arc1_center.y + a * uy,
1065 };
1066
1067 let intersection1 = Coords2d {
1069 x: mid_point.x + h * px,
1070 y: mid_point.y + h * py,
1071 };
1072 let intersection2 = Coords2d {
1073 x: mid_point.x - h * px,
1074 y: mid_point.y - h * py,
1075 };
1076
1077 let mut candidates: Vec<Coords2d> = Vec::new();
1079
1080 if is_point_on_arc(intersection1, arc1_center, arc1_start, arc1_end, epsilon)
1081 && is_point_on_arc(intersection1, arc2_center, arc2_start, arc2_end, epsilon)
1082 {
1083 candidates.push(intersection1);
1084 }
1085
1086 if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1087 if is_point_on_arc(intersection2, arc1_center, arc1_start, arc1_end, epsilon)
1089 && is_point_on_arc(intersection2, arc2_center, arc2_start, arc2_end, epsilon)
1090 {
1091 candidates.push(intersection2);
1092 }
1093 }
1094
1095 candidates
1096}
1097
1098pub fn arc_arc_intersection(
1103 arc1_center: Coords2d,
1104 arc1_start: Coords2d,
1105 arc1_end: Coords2d,
1106 arc2_center: Coords2d,
1107 arc2_start: Coords2d,
1108 arc2_end: Coords2d,
1109 epsilon: f64,
1110) -> Option<Coords2d> {
1111 arc_arc_intersections(
1112 arc1_center,
1113 arc1_start,
1114 arc1_end,
1115 arc2_center,
1116 arc2_start,
1117 arc2_end,
1118 epsilon,
1119 )
1120 .first()
1121 .copied()
1122}
1123
1124pub fn circle_arc_intersections(
1128 circle_center: Coords2d,
1129 circle_radius: f64,
1130 arc_center: Coords2d,
1131 arc_start: Coords2d,
1132 arc_end: Coords2d,
1133 epsilon: f64,
1134) -> Vec<Coords2d> {
1135 let r1 = circle_radius;
1136 let r2 = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
1137 + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
1138 .sqrt();
1139
1140 let dx = arc_center.x - circle_center.x;
1141 let dy = arc_center.y - circle_center.y;
1142 let d = (dx * dx + dy * dy).sqrt();
1143
1144 if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon || d < EPSILON_PARALLEL {
1145 return Vec::new();
1146 }
1147
1148 let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1149 let h_sq = r1 * r1 - a * a;
1150 if h_sq < 0.0 {
1151 return Vec::new();
1152 }
1153 let h = h_sq.sqrt();
1154 if h.is_nan() {
1155 return Vec::new();
1156 }
1157
1158 let ux = dx / d;
1159 let uy = dy / d;
1160 let px = -uy;
1161 let py = ux;
1162 let mid_point = Coords2d {
1163 x: circle_center.x + a * ux,
1164 y: circle_center.y + a * uy,
1165 };
1166
1167 let intersection1 = Coords2d {
1168 x: mid_point.x + h * px,
1169 y: mid_point.y + h * py,
1170 };
1171 let intersection2 = Coords2d {
1172 x: mid_point.x - h * px,
1173 y: mid_point.y - h * py,
1174 };
1175
1176 let mut intersections = Vec::new();
1177 if is_point_on_arc(intersection1, arc_center, arc_start, arc_end, epsilon) {
1178 intersections.push(intersection1);
1179 }
1180 if ((intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon)
1181 && is_point_on_arc(intersection2, arc_center, arc_start, arc_end, epsilon)
1182 {
1183 intersections.push(intersection2);
1184 }
1185 intersections
1186}
1187
1188pub fn circle_circle_intersections(
1192 circle1_center: Coords2d,
1193 circle1_radius: f64,
1194 circle2_center: Coords2d,
1195 circle2_radius: f64,
1196 epsilon: f64,
1197) -> Vec<Coords2d> {
1198 let dx = circle2_center.x - circle1_center.x;
1199 let dy = circle2_center.y - circle1_center.y;
1200 let d = (dx * dx + dy * dy).sqrt();
1201
1202 if d > circle1_radius + circle2_radius + epsilon
1203 || d < (circle1_radius - circle2_radius).abs() - epsilon
1204 || d < EPSILON_PARALLEL
1205 {
1206 return Vec::new();
1207 }
1208
1209 let a = (circle1_radius * circle1_radius - circle2_radius * circle2_radius + d * d) / (2.0 * d);
1210 let h_sq = circle1_radius * circle1_radius - a * a;
1211 if h_sq < 0.0 {
1212 return Vec::new();
1213 }
1214
1215 let h = if h_sq <= epsilon { 0.0 } else { h_sq.sqrt() };
1216 if h.is_nan() {
1217 return Vec::new();
1218 }
1219
1220 let ux = dx / d;
1221 let uy = dy / d;
1222 let px = -uy;
1223 let py = ux;
1224
1225 let mid_point = Coords2d {
1226 x: circle1_center.x + a * ux,
1227 y: circle1_center.y + a * uy,
1228 };
1229
1230 let intersection1 = Coords2d {
1231 x: mid_point.x + h * px,
1232 y: mid_point.y + h * py,
1233 };
1234 let intersection2 = Coords2d {
1235 x: mid_point.x - h * px,
1236 y: mid_point.y - h * py,
1237 };
1238
1239 let mut intersections = vec![intersection1];
1240 if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1241 intersections.push(intersection2);
1242 }
1243 intersections
1244}
1245
1246fn get_point_coords_from_native(objects: &[Object], point_id: ObjectId, default_unit: UnitLength) -> Option<Coords2d> {
1249 let point_obj = objects.get(point_id.0)?;
1250
1251 let ObjectKind::Segment { segment } = &point_obj.kind else {
1253 return None;
1254 };
1255
1256 let Segment::Point(point) = segment else {
1257 return None;
1258 };
1259
1260 Some(Coords2d {
1262 x: number_to_unit(&point.position.x, default_unit),
1263 y: number_to_unit(&point.position.y, default_unit),
1264 })
1265}
1266
1267pub fn get_position_coords_for_line(
1270 segment_obj: &Object,
1271 which: LineEndpoint,
1272 objects: &[Object],
1273 default_unit: UnitLength,
1274) -> Option<Coords2d> {
1275 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1276 return None;
1277 };
1278
1279 let Segment::Line(line) = segment else {
1280 return None;
1281 };
1282
1283 let point_id = match which {
1285 LineEndpoint::Start => line.start,
1286 LineEndpoint::End => line.end,
1287 };
1288
1289 get_point_coords_from_native(objects, point_id, default_unit)
1290}
1291
1292fn is_point_coincident_with_segment_native(point_id: ObjectId, segment_id: ObjectId, objects: &[Object]) -> bool {
1294 for obj in objects {
1296 let ObjectKind::Constraint { constraint } = &obj.kind else {
1297 continue;
1298 };
1299
1300 let Constraint::Coincident(coincident) = constraint else {
1301 continue;
1302 };
1303
1304 let has_point = coincident.contains_segment(point_id);
1306 let has_segment = coincident.contains_segment(segment_id);
1307
1308 if has_point && has_segment {
1309 return true;
1310 }
1311 }
1312 false
1313}
1314
1315pub fn get_position_coords_from_arc(
1317 segment_obj: &Object,
1318 which: ArcPoint,
1319 objects: &[Object],
1320 default_unit: UnitLength,
1321) -> Option<Coords2d> {
1322 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1323 return None;
1324 };
1325
1326 let Segment::Arc(arc) = segment else {
1327 return None;
1328 };
1329
1330 let point_id = match which {
1332 ArcPoint::Start => arc.start,
1333 ArcPoint::End => arc.end,
1334 ArcPoint::Center => arc.center,
1335 };
1336
1337 get_point_coords_from_native(objects, point_id, default_unit)
1338}
1339
1340pub fn get_position_coords_from_circle(
1342 segment_obj: &Object,
1343 which: CirclePoint,
1344 objects: &[Object],
1345 default_unit: UnitLength,
1346) -> Option<Coords2d> {
1347 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1348 return None;
1349 };
1350
1351 let Segment::Circle(circle) = segment else {
1352 return None;
1353 };
1354
1355 let point_id = match which {
1356 CirclePoint::Start => circle.start,
1357 CirclePoint::Center => circle.center,
1358 };
1359
1360 get_point_coords_from_native(objects, point_id, default_unit)
1361}
1362
1363#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1365enum CurveKind {
1366 Line,
1367 Circular,
1368}
1369
1370#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1372enum CurveDomain {
1373 Open,
1374 Closed,
1375}
1376
1377#[derive(Debug, Clone, Copy)]
1379struct CurveHandle {
1380 segment_id: ObjectId,
1381 kind: CurveKind,
1382 domain: CurveDomain,
1383 start: Coords2d,
1384 end: Coords2d,
1385 center: Option<Coords2d>,
1386 radius: Option<f64>,
1387}
1388
1389impl CurveHandle {
1390 fn project_for_trim(self, point: Coords2d) -> Result<f64, String> {
1391 match (self.kind, self.domain) {
1392 (CurveKind::Line, CurveDomain::Open) => Ok(project_point_onto_segment(point, self.start, self.end)),
1393 (CurveKind::Circular, CurveDomain::Open) => {
1394 let center = self
1395 .center
1396 .ok_or_else(|| format!("Curve {} missing center for arc projection", self.segment_id.0))?;
1397 Ok(project_point_onto_arc(point, center, self.start, self.end))
1398 }
1399 (CurveKind::Circular, CurveDomain::Closed) => {
1400 let center = self
1401 .center
1402 .ok_or_else(|| format!("Curve {} missing center for circle projection", self.segment_id.0))?;
1403 Ok(project_point_onto_circle(point, center, self.start))
1404 }
1405 (CurveKind::Line, CurveDomain::Closed) => Err(format!(
1406 "Invalid curve state: line {} cannot be closed",
1407 self.segment_id.0
1408 )),
1409 }
1410 }
1411}
1412
1413fn load_curve_handle(
1415 segment_obj: &Object,
1416 objects: &[Object],
1417 default_unit: UnitLength,
1418) -> Result<CurveHandle, String> {
1419 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1420 return Err("Object is not a segment".to_owned());
1421 };
1422
1423 match segment {
1424 Segment::Line(_) => {
1425 let start = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit)
1426 .ok_or_else(|| format!("Could not get line start for segment {}", segment_obj.id.0))?;
1427 let end = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit)
1428 .ok_or_else(|| format!("Could not get line end for segment {}", segment_obj.id.0))?;
1429 Ok(CurveHandle {
1430 segment_id: segment_obj.id,
1431 kind: CurveKind::Line,
1432 domain: CurveDomain::Open,
1433 start,
1434 end,
1435 center: None,
1436 radius: None,
1437 })
1438 }
1439 Segment::Arc(_) => {
1440 let start = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit)
1441 .ok_or_else(|| format!("Could not get arc start for segment {}", segment_obj.id.0))?;
1442 let end = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit)
1443 .ok_or_else(|| format!("Could not get arc end for segment {}", segment_obj.id.0))?;
1444 let center = get_position_coords_from_arc(segment_obj, ArcPoint::Center, objects, default_unit)
1445 .ok_or_else(|| format!("Could not get arc center for segment {}", segment_obj.id.0))?;
1446 let radius =
1447 ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1448 Ok(CurveHandle {
1449 segment_id: segment_obj.id,
1450 kind: CurveKind::Circular,
1451 domain: CurveDomain::Open,
1452 start,
1453 end,
1454 center: Some(center),
1455 radius: Some(radius),
1456 })
1457 }
1458 Segment::Circle(_) => {
1459 let start = get_position_coords_from_circle(segment_obj, CirclePoint::Start, objects, default_unit)
1460 .ok_or_else(|| format!("Could not get circle start for segment {}", segment_obj.id.0))?;
1461 let center = get_position_coords_from_circle(segment_obj, CirclePoint::Center, objects, default_unit)
1462 .ok_or_else(|| format!("Could not get circle center for segment {}", segment_obj.id.0))?;
1463 let radius =
1464 ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1465 Ok(CurveHandle {
1466 segment_id: segment_obj.id,
1467 kind: CurveKind::Circular,
1468 domain: CurveDomain::Closed,
1469 start,
1470 end: start,
1472 center: Some(center),
1473 radius: Some(radius),
1474 })
1475 }
1476 Segment::Point(_) => Err(format!(
1477 "Point segment {} cannot be used as trim curve",
1478 segment_obj.id.0
1479 )),
1480 }
1481}
1482
1483fn project_point_onto_curve(curve: CurveHandle, point: Coords2d) -> Result<f64, String> {
1484 curve.project_for_trim(point)
1485}
1486
1487fn curve_contains_point(curve: CurveHandle, point: Coords2d, epsilon: f64) -> bool {
1488 match (curve.kind, curve.domain) {
1489 (CurveKind::Line, CurveDomain::Open) => {
1490 let t = project_point_onto_segment(point, curve.start, curve.end);
1491 (0.0..=1.0).contains(&t) && perpendicular_distance_to_segment(point, curve.start, curve.end) <= epsilon
1492 }
1493 (CurveKind::Circular, CurveDomain::Open) => curve
1494 .center
1495 .is_some_and(|center| is_point_on_arc(point, center, curve.start, curve.end, epsilon)),
1496 (CurveKind::Circular, CurveDomain::Closed) => curve.center.is_some_and(|center| {
1497 let radius = curve
1498 .radius
1499 .unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
1500 is_point_on_circle(point, center, radius, epsilon)
1501 }),
1502 (CurveKind::Line, CurveDomain::Closed) => false,
1503 }
1504}
1505
1506fn curve_line_segment_intersections(
1507 curve: CurveHandle,
1508 line_start: Coords2d,
1509 line_end: Coords2d,
1510 epsilon: f64,
1511) -> Vec<(f64, Coords2d)> {
1512 match (curve.kind, curve.domain) {
1513 (CurveKind::Line, CurveDomain::Open) => {
1514 line_segment_intersection(line_start, line_end, curve.start, curve.end, epsilon)
1515 .map(|intersection| {
1516 (
1517 project_point_onto_segment(intersection, line_start, line_end),
1518 intersection,
1519 )
1520 })
1521 .into_iter()
1522 .collect()
1523 }
1524 (CurveKind::Circular, CurveDomain::Open) => curve
1525 .center
1526 .and_then(|center| line_arc_intersection(line_start, line_end, center, curve.start, curve.end, epsilon))
1527 .map(|intersection| {
1528 (
1529 project_point_onto_segment(intersection, line_start, line_end),
1530 intersection,
1531 )
1532 })
1533 .into_iter()
1534 .collect(),
1535 (CurveKind::Circular, CurveDomain::Closed) => {
1536 let Some(center) = curve.center else {
1537 return Vec::new();
1538 };
1539 let radius = curve
1540 .radius
1541 .unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
1542 line_circle_intersections(line_start, line_end, center, radius, epsilon)
1543 }
1544 (CurveKind::Line, CurveDomain::Closed) => Vec::new(),
1545 }
1546}
1547
1548fn curve_polyline_intersections(curve: CurveHandle, polyline: &[Coords2d], epsilon: f64) -> Vec<(Coords2d, usize)> {
1549 let mut intersections = Vec::new();
1550
1551 for i in 0..polyline.len().saturating_sub(1) {
1552 let p1 = polyline[i];
1553 let p2 = polyline[i + 1];
1554 for (_, intersection) in curve_line_segment_intersections(curve, p1, p2, epsilon) {
1555 intersections.push((intersection, i));
1556 }
1557 }
1558
1559 intersections
1560}
1561
1562fn curve_curve_intersections(curve: CurveHandle, other: CurveHandle, epsilon: f64) -> Vec<Coords2d> {
1563 match (curve.kind, curve.domain, other.kind, other.domain) {
1564 (CurveKind::Line, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => {
1565 line_segment_intersection(curve.start, curve.end, other.start, other.end, epsilon)
1566 .into_iter()
1567 .collect()
1568 }
1569 (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => other
1570 .center
1571 .and_then(|other_center| {
1572 line_arc_intersection(curve.start, curve.end, other_center, other.start, other.end, epsilon)
1573 })
1574 .into_iter()
1575 .collect(),
1576 (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
1577 let Some(other_center) = other.center else {
1578 return Vec::new();
1579 };
1580 let other_radius = other.radius.unwrap_or_else(|| {
1581 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1582 });
1583 line_circle_intersections(curve.start, curve.end, other_center, other_radius, epsilon)
1584 .into_iter()
1585 .map(|(_, point)| point)
1586 .collect()
1587 }
1588 (CurveKind::Circular, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => curve
1589 .center
1590 .and_then(|curve_center| {
1591 line_arc_intersection(other.start, other.end, curve_center, curve.start, curve.end, epsilon)
1592 })
1593 .into_iter()
1594 .collect(),
1595 (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => {
1596 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1597 return Vec::new();
1598 };
1599 arc_arc_intersections(
1600 curve_center,
1601 curve.start,
1602 curve.end,
1603 other_center,
1604 other.start,
1605 other.end,
1606 epsilon,
1607 )
1608 }
1609 (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
1610 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1611 return Vec::new();
1612 };
1613 let other_radius = other.radius.unwrap_or_else(|| {
1614 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1615 });
1616 circle_arc_intersections(
1617 other_center,
1618 other_radius,
1619 curve_center,
1620 curve.start,
1621 curve.end,
1622 epsilon,
1623 )
1624 }
1625 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Line, CurveDomain::Open) => {
1626 let Some(curve_center) = curve.center else {
1627 return Vec::new();
1628 };
1629 let curve_radius = curve.radius.unwrap_or_else(|| {
1630 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1631 });
1632 line_circle_intersections(other.start, other.end, curve_center, curve_radius, epsilon)
1633 .into_iter()
1634 .map(|(_, point)| point)
1635 .collect()
1636 }
1637 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Open) => {
1638 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1639 return Vec::new();
1640 };
1641 let curve_radius = curve.radius.unwrap_or_else(|| {
1642 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1643 });
1644 circle_arc_intersections(
1645 curve_center,
1646 curve_radius,
1647 other_center,
1648 other.start,
1649 other.end,
1650 epsilon,
1651 )
1652 }
1653 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Closed) => {
1654 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1655 return Vec::new();
1656 };
1657 let curve_radius = curve.radius.unwrap_or_else(|| {
1658 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1659 });
1660 let other_radius = other.radius.unwrap_or_else(|| {
1661 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1662 });
1663 circle_circle_intersections(curve_center, curve_radius, other_center, other_radius, epsilon)
1664 }
1665 _ => Vec::new(),
1666 }
1667}
1668
1669fn segment_endpoint_points(
1670 segment_obj: &Object,
1671 objects: &[Object],
1672 default_unit: UnitLength,
1673) -> Vec<(ObjectId, Coords2d)> {
1674 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1675 return Vec::new();
1676 };
1677
1678 match segment {
1679 Segment::Line(line) => {
1680 let mut points = Vec::new();
1681 if let Some(start) = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit) {
1682 points.push((line.start, start));
1683 }
1684 if let Some(end) = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit) {
1685 points.push((line.end, end));
1686 }
1687 points
1688 }
1689 Segment::Arc(arc) => {
1690 let mut points = Vec::new();
1691 if let Some(start) = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit) {
1692 points.push((arc.start, start));
1693 }
1694 if let Some(end) = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit) {
1695 points.push((arc.end, end));
1696 }
1697 points
1698 }
1699 _ => Vec::new(),
1700 }
1701}
1702
1703pub fn get_next_trim_spawn(
1731 points: &[Coords2d],
1732 start_index: usize,
1733 objects: &[Object],
1734 default_unit: UnitLength,
1735) -> TrimItem {
1736 let scene_curves: Vec<CurveHandle> = objects
1737 .iter()
1738 .filter_map(|obj| load_curve_handle(obj, objects, default_unit).ok())
1739 .collect();
1740
1741 for i in start_index..points.len().saturating_sub(1) {
1743 let p1 = points[i];
1744 let p2 = points[i + 1];
1745
1746 for curve in &scene_curves {
1748 let intersections = curve_line_segment_intersections(*curve, p1, p2, EPSILON_POINT_ON_SEGMENT);
1749 if let Some((_, intersection)) = intersections.first() {
1750 return TrimItem::Spawn {
1751 trim_spawn_seg_id: curve.segment_id,
1752 trim_spawn_coords: *intersection,
1753 next_index: i,
1754 };
1755 }
1756 }
1757 }
1758
1759 TrimItem::None {
1761 next_index: points.len().saturating_sub(1),
1762 }
1763}
1764
1765pub fn get_trim_spawn_terminations(
1820 trim_spawn_seg_id: ObjectId,
1821 trim_spawn_coords: &[Coords2d],
1822 objects: &[Object],
1823 default_unit: UnitLength,
1824) -> Result<TrimTerminations, String> {
1825 let trim_spawn_seg = objects.iter().find(|obj| obj.id == trim_spawn_seg_id);
1827
1828 let trim_spawn_seg = match trim_spawn_seg {
1829 Some(seg) => seg,
1830 None => {
1831 return Err(format!("Trim spawn segment {} not found", trim_spawn_seg_id.0));
1832 }
1833 };
1834
1835 let trim_curve = load_curve_handle(trim_spawn_seg, objects, default_unit).map_err(|e| {
1836 format!(
1837 "Failed to load trim spawn segment {} as normalized curve: {}",
1838 trim_spawn_seg_id.0, e
1839 )
1840 })?;
1841
1842 let all_intersections = curve_polyline_intersections(trim_curve, trim_spawn_coords, EPSILON_POINT_ON_SEGMENT);
1847
1848 let intersection_point = if all_intersections.is_empty() {
1851 return Err("Could not find intersection point between polyline and trim spawn segment".to_string());
1852 } else {
1853 let mid_index = (trim_spawn_coords.len() - 1) / 2;
1855 let mid_point = trim_spawn_coords[mid_index];
1856
1857 let mut min_dist = f64::INFINITY;
1859 let mut closest_intersection = all_intersections[0].0;
1860
1861 for (intersection, _) in &all_intersections {
1862 let dist = ((intersection.x - mid_point.x) * (intersection.x - mid_point.x)
1863 + (intersection.y - mid_point.y) * (intersection.y - mid_point.y))
1864 .sqrt();
1865 if dist < min_dist {
1866 min_dist = dist;
1867 closest_intersection = *intersection;
1868 }
1869 }
1870
1871 closest_intersection
1872 };
1873
1874 let intersection_t = project_point_onto_curve(trim_curve, intersection_point)?;
1876
1877 let left_termination = find_termination_in_direction(
1879 trim_spawn_seg,
1880 trim_curve,
1881 intersection_t,
1882 TrimDirection::Left,
1883 objects,
1884 default_unit,
1885 )?;
1886
1887 let right_termination = find_termination_in_direction(
1888 trim_spawn_seg,
1889 trim_curve,
1890 intersection_t,
1891 TrimDirection::Right,
1892 objects,
1893 default_unit,
1894 )?;
1895
1896 Ok(TrimTerminations {
1897 left_side: left_termination,
1898 right_side: right_termination,
1899 })
1900}
1901
1902fn find_termination_in_direction(
1955 trim_spawn_seg: &Object,
1956 trim_curve: CurveHandle,
1957 intersection_t: f64,
1958 direction: TrimDirection,
1959 objects: &[Object],
1960 default_unit: UnitLength,
1961) -> Result<TrimTermination, String> {
1962 let ObjectKind::Segment { segment } = &trim_spawn_seg.kind else {
1964 return Err("Trim spawn segment is not a segment".to_string());
1965 };
1966
1967 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1969 enum CandidateType {
1970 Intersection,
1971 Coincident,
1972 Endpoint,
1973 }
1974
1975 #[derive(Debug, Clone)]
1976 struct Candidate {
1977 t: f64,
1978 point: Coords2d,
1979 candidate_type: CandidateType,
1980 segment_id: Option<ObjectId>,
1981 point_id: Option<ObjectId>,
1982 }
1983
1984 let mut candidates: Vec<Candidate> = Vec::new();
1985
1986 match segment {
1988 Segment::Line(line) => {
1989 candidates.push(Candidate {
1990 t: 0.0,
1991 point: trim_curve.start,
1992 candidate_type: CandidateType::Endpoint,
1993 segment_id: None,
1994 point_id: Some(line.start),
1995 });
1996 candidates.push(Candidate {
1997 t: 1.0,
1998 point: trim_curve.end,
1999 candidate_type: CandidateType::Endpoint,
2000 segment_id: None,
2001 point_id: Some(line.end),
2002 });
2003 }
2004 Segment::Arc(arc) => {
2005 candidates.push(Candidate {
2007 t: 0.0,
2008 point: trim_curve.start,
2009 candidate_type: CandidateType::Endpoint,
2010 segment_id: None,
2011 point_id: Some(arc.start),
2012 });
2013 candidates.push(Candidate {
2014 t: 1.0,
2015 point: trim_curve.end,
2016 candidate_type: CandidateType::Endpoint,
2017 segment_id: None,
2018 point_id: Some(arc.end),
2019 });
2020 }
2021 Segment::Circle(_) => {
2022 }
2024 _ => {}
2025 }
2026
2027 let trim_spawn_seg_id = trim_spawn_seg.id;
2029
2030 for other_seg in objects.iter() {
2032 let other_id = other_seg.id;
2033 if other_id == trim_spawn_seg_id {
2034 continue;
2035 }
2036
2037 if let Ok(other_curve) = load_curve_handle(other_seg, objects, default_unit) {
2038 for intersection in curve_curve_intersections(trim_curve, other_curve, EPSILON_POINT_ON_SEGMENT) {
2039 let Ok(t) = project_point_onto_curve(trim_curve, intersection) else {
2040 continue;
2041 };
2042 candidates.push(Candidate {
2043 t,
2044 point: intersection,
2045 candidate_type: CandidateType::Intersection,
2046 segment_id: Some(other_id),
2047 point_id: None,
2048 });
2049 }
2050 }
2051
2052 for (other_point_id, other_point) in segment_endpoint_points(other_seg, objects, default_unit) {
2053 if !is_point_coincident_with_segment_native(other_point_id, trim_spawn_seg_id, objects) {
2054 continue;
2055 }
2056 if !curve_contains_point(trim_curve, other_point, EPSILON_POINT_ON_SEGMENT) {
2057 continue;
2058 }
2059 let Ok(t) = project_point_onto_curve(trim_curve, other_point) else {
2060 continue;
2061 };
2062 candidates.push(Candidate {
2063 t,
2064 point: other_point,
2065 candidate_type: CandidateType::Coincident,
2066 segment_id: Some(other_id),
2067 point_id: Some(other_point_id),
2068 });
2069 }
2070 }
2071
2072 let is_circle_segment = trim_curve.domain == CurveDomain::Closed;
2073
2074 let intersection_epsilon = EPSILON_POINT_ON_SEGMENT * 10.0; let direction_distance = |candidate_t: f64| -> f64 {
2078 if is_circle_segment {
2079 match direction {
2080 TrimDirection::Left => (intersection_t - candidate_t).rem_euclid(1.0),
2081 TrimDirection::Right => (candidate_t - intersection_t).rem_euclid(1.0),
2082 }
2083 } else {
2084 (candidate_t - intersection_t).abs()
2085 }
2086 };
2087 let filtered_candidates: Vec<Candidate> = candidates
2088 .into_iter()
2089 .filter(|candidate| {
2090 let dist_from_intersection = if is_circle_segment {
2091 let ccw = (candidate.t - intersection_t).rem_euclid(1.0);
2092 let cw = (intersection_t - candidate.t).rem_euclid(1.0);
2093 libm::fmin(ccw, cw)
2094 } else {
2095 (candidate.t - intersection_t).abs()
2096 };
2097 if dist_from_intersection < intersection_epsilon {
2098 return false; }
2100
2101 if is_circle_segment {
2102 direction_distance(candidate.t) > intersection_epsilon
2103 } else {
2104 match direction {
2105 TrimDirection::Left => candidate.t < intersection_t,
2106 TrimDirection::Right => candidate.t > intersection_t,
2107 }
2108 }
2109 })
2110 .collect();
2111
2112 let mut sorted_candidates = filtered_candidates;
2115 sorted_candidates.sort_by(|a, b| {
2116 let dist_a = direction_distance(a.t);
2117 let dist_b = direction_distance(b.t);
2118 let dist_diff = dist_a - dist_b;
2119 if dist_diff.abs() > EPSILON_POINT_ON_SEGMENT {
2120 dist_diff.partial_cmp(&0.0).unwrap_or(std::cmp::Ordering::Equal)
2121 } else {
2122 let type_priority = |candidate_type: CandidateType| -> i32 {
2124 match candidate_type {
2125 CandidateType::Coincident => 0,
2126 CandidateType::Intersection => 1,
2127 CandidateType::Endpoint => 2,
2128 }
2129 };
2130 type_priority(a.candidate_type).cmp(&type_priority(b.candidate_type))
2131 }
2132 });
2133
2134 let closest_candidate = match sorted_candidates.first() {
2136 Some(c) => c,
2137 None => {
2138 if is_circle_segment {
2139 return Err("No trim termination candidate found for circle".to_string());
2140 }
2141 let endpoint = match direction {
2143 TrimDirection::Left => trim_curve.start,
2144 TrimDirection::Right => trim_curve.end,
2145 };
2146 return Ok(TrimTermination::SegEndPoint {
2147 trim_termination_coords: endpoint,
2148 });
2149 }
2150 };
2151
2152 if !is_circle_segment
2156 && closest_candidate.candidate_type == CandidateType::Intersection
2157 && let Some(seg_id) = closest_candidate.segment_id
2158 {
2159 let intersecting_seg = objects.iter().find(|obj| obj.id == seg_id);
2160
2161 if let Some(intersecting_seg) = intersecting_seg {
2162 let endpoint_epsilon = EPSILON_POINT_ON_SEGMENT * 1000.0; let is_other_seg_endpoint = segment_endpoint_points(intersecting_seg, objects, default_unit)
2165 .into_iter()
2166 .any(|(_, endpoint)| {
2167 let dist_to_endpoint = ((closest_candidate.point.x - endpoint.x).powi(2)
2168 + (closest_candidate.point.y - endpoint.y).powi(2))
2169 .sqrt();
2170 dist_to_endpoint < endpoint_epsilon
2171 });
2172
2173 if is_other_seg_endpoint {
2176 let endpoint = match direction {
2177 TrimDirection::Left => trim_curve.start,
2178 TrimDirection::Right => trim_curve.end,
2179 };
2180 return Ok(TrimTermination::SegEndPoint {
2181 trim_termination_coords: endpoint,
2182 });
2183 }
2184 }
2185
2186 let endpoint_t = match direction {
2188 TrimDirection::Left => 0.0,
2189 TrimDirection::Right => 1.0,
2190 };
2191 let endpoint = match direction {
2192 TrimDirection::Left => trim_curve.start,
2193 TrimDirection::Right => trim_curve.end,
2194 };
2195 let dist_to_endpoint_param = (closest_candidate.t - endpoint_t).abs();
2196 let dist_to_endpoint_coords = ((closest_candidate.point.x - endpoint.x)
2197 * (closest_candidate.point.x - endpoint.x)
2198 + (closest_candidate.point.y - endpoint.y) * (closest_candidate.point.y - endpoint.y))
2199 .sqrt();
2200
2201 let is_at_endpoint =
2202 dist_to_endpoint_param < EPSILON_POINT_ON_SEGMENT || dist_to_endpoint_coords < EPSILON_POINT_ON_SEGMENT;
2203
2204 if is_at_endpoint {
2205 return Ok(TrimTermination::SegEndPoint {
2207 trim_termination_coords: endpoint,
2208 });
2209 }
2210 }
2211
2212 let endpoint_t_for_return = match direction {
2214 TrimDirection::Left => 0.0,
2215 TrimDirection::Right => 1.0,
2216 };
2217 if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Intersection {
2218 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2219 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2220 let endpoint = match direction {
2223 TrimDirection::Left => trim_curve.start,
2224 TrimDirection::Right => trim_curve.end,
2225 };
2226 return Ok(TrimTermination::SegEndPoint {
2227 trim_termination_coords: endpoint,
2228 });
2229 }
2230 }
2231
2232 let endpoint = match direction {
2234 TrimDirection::Left => trim_curve.start,
2235 TrimDirection::Right => trim_curve.end,
2236 };
2237 if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Endpoint {
2238 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2239 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2240 return Ok(TrimTermination::SegEndPoint {
2242 trim_termination_coords: endpoint,
2243 });
2244 }
2245 }
2246
2247 if closest_candidate.candidate_type == CandidateType::Coincident {
2249 Ok(TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2251 trim_termination_coords: closest_candidate.point,
2252 intersecting_seg_id: closest_candidate
2253 .segment_id
2254 .ok_or_else(|| "Missing segment_id for coincident".to_string())?,
2255 other_segment_point_id: closest_candidate
2256 .point_id
2257 .ok_or_else(|| "Missing point_id for coincident".to_string())?,
2258 })
2259 } else if closest_candidate.candidate_type == CandidateType::Intersection {
2260 Ok(TrimTermination::Intersection {
2261 trim_termination_coords: closest_candidate.point,
2262 intersecting_seg_id: closest_candidate
2263 .segment_id
2264 .ok_or_else(|| "Missing segment_id for intersection".to_string())?,
2265 })
2266 } else {
2267 if is_circle_segment {
2268 return Err("Circle trim termination unexpectedly resolved to endpoint".to_string());
2269 }
2270 Ok(TrimTermination::SegEndPoint {
2272 trim_termination_coords: closest_candidate.point,
2273 })
2274 }
2275}
2276
2277#[cfg(test)]
2288#[allow(dead_code)]
2289pub(crate) async fn execute_trim_loop<F, Fut>(
2290 points: &[Coords2d],
2291 default_unit: UnitLength,
2292 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2293 mut execute_operations: F,
2294) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>
2295where
2296 F: FnMut(Vec<TrimOperation>, crate::frontend::api::SceneGraphDelta) -> Fut,
2297 Fut: std::future::Future<
2298 Output = Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>,
2299 >,
2300{
2301 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2303 let points = normalized_points.as_slice();
2304
2305 let mut start_index = 0;
2306 let max_iterations = 1000;
2307 let mut iteration_count = 0;
2308 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2309 crate::frontend::api::SourceDelta { text: String::new() },
2310 initial_scene_graph_delta.clone(),
2311 ));
2312 let mut invalidates_ids = false;
2313 let mut current_scene_graph_delta = initial_scene_graph_delta;
2314 let circle_delete_fallback_strategy =
2315 |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
2316 if !error.contains("No trim termination candidate found for circle") {
2317 return None;
2318 }
2319 let is_circle = scene_objects
2320 .iter()
2321 .find(|obj| obj.id == segment_id)
2322 .is_some_and(|obj| {
2323 matches!(
2324 obj.kind,
2325 ObjectKind::Segment {
2326 segment: Segment::Circle(_)
2327 }
2328 )
2329 });
2330 if is_circle {
2331 Some(vec![TrimOperation::SimpleTrim {
2332 segment_to_trim_id: segment_id,
2333 }])
2334 } else {
2335 None
2336 }
2337 };
2338
2339 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2340 iteration_count += 1;
2341
2342 let next_trim_spawn = get_next_trim_spawn(
2344 points,
2345 start_index,
2346 ¤t_scene_graph_delta.new_graph.objects,
2347 default_unit,
2348 );
2349
2350 match &next_trim_spawn {
2351 TrimItem::None { next_index } => {
2352 let old_start_index = start_index;
2353 start_index = *next_index;
2354
2355 if start_index <= old_start_index {
2357 start_index = old_start_index + 1;
2358 }
2359
2360 if start_index >= points.len().saturating_sub(1) {
2362 break;
2363 }
2364 continue;
2365 }
2366 TrimItem::Spawn {
2367 trim_spawn_seg_id,
2368 trim_spawn_coords,
2369 next_index,
2370 ..
2371 } => {
2372 let terminations = match get_trim_spawn_terminations(
2374 *trim_spawn_seg_id,
2375 points,
2376 ¤t_scene_graph_delta.new_graph.objects,
2377 default_unit,
2378 ) {
2379 Ok(terms) => terms,
2380 Err(e) => {
2381 crate::logln!("Error getting trim spawn terminations: {}", e);
2382 if let Some(strategy) = circle_delete_fallback_strategy(
2383 &e,
2384 *trim_spawn_seg_id,
2385 ¤t_scene_graph_delta.new_graph.objects,
2386 ) {
2387 match execute_operations(strategy, current_scene_graph_delta.clone()).await {
2388 Ok((source_delta, scene_graph_delta)) => {
2389 last_result = Some((source_delta, scene_graph_delta.clone()));
2390 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2391 current_scene_graph_delta = scene_graph_delta;
2392 }
2393 Err(exec_err) => {
2394 crate::logln!(
2395 "Error executing circle-delete fallback trim operation: {}",
2396 exec_err
2397 );
2398 }
2399 }
2400
2401 let old_start_index = start_index;
2402 start_index = *next_index;
2403 if start_index <= old_start_index {
2404 start_index = old_start_index + 1;
2405 }
2406 continue;
2407 }
2408
2409 let old_start_index = start_index;
2410 start_index = *next_index;
2411 if start_index <= old_start_index {
2412 start_index = old_start_index + 1;
2413 }
2414 continue;
2415 }
2416 };
2417
2418 let trim_spawn_segment = current_scene_graph_delta
2420 .new_graph
2421 .objects
2422 .iter()
2423 .find(|obj| obj.id == *trim_spawn_seg_id)
2424 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
2425
2426 let plan = match build_trim_plan(
2427 *trim_spawn_seg_id,
2428 *trim_spawn_coords,
2429 trim_spawn_segment,
2430 &terminations.left_side,
2431 &terminations.right_side,
2432 ¤t_scene_graph_delta.new_graph.objects,
2433 default_unit,
2434 ) {
2435 Ok(plan) => plan,
2436 Err(e) => {
2437 crate::logln!("Error determining trim strategy: {}", e);
2438 let old_start_index = start_index;
2439 start_index = *next_index;
2440 if start_index <= old_start_index {
2441 start_index = old_start_index + 1;
2442 }
2443 continue;
2444 }
2445 };
2446 let strategy = lower_trim_plan(&plan);
2447
2448 let geometry_was_modified = trim_plan_modifies_geometry(&plan);
2451
2452 match execute_operations(strategy, current_scene_graph_delta.clone()).await {
2454 Ok((source_delta, scene_graph_delta)) => {
2455 last_result = Some((source_delta, scene_graph_delta.clone()));
2456 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2457 current_scene_graph_delta = scene_graph_delta;
2458 }
2459 Err(e) => {
2460 crate::logln!("Error executing trim operations: {}", e);
2461 }
2463 }
2464
2465 let old_start_index = start_index;
2467 start_index = *next_index;
2468
2469 if start_index <= old_start_index && !geometry_was_modified {
2471 start_index = old_start_index + 1;
2472 }
2473 }
2474 }
2475 }
2476
2477 if iteration_count >= max_iterations {
2478 return Err(format!("Reached max iterations ({})", max_iterations));
2479 }
2480
2481 last_result.ok_or_else(|| "No trim operations were executed".to_string())
2483}
2484
2485#[cfg(all(feature = "artifact-graph", test))]
2487#[derive(Debug, Clone)]
2488pub struct TrimFlowResult {
2489 pub kcl_code: String,
2490 pub invalidates_ids: bool,
2491}
2492
2493#[cfg(all(not(target_arch = "wasm32"), feature = "artifact-graph", test))]
2509pub(crate) async fn execute_trim_flow(
2510 kcl_code: &str,
2511 trim_points: &[Coords2d],
2512 sketch_id: ObjectId,
2513) -> Result<TrimFlowResult, String> {
2514 use crate::ExecutorContext;
2515 use crate::Program;
2516 use crate::execution::MockConfig;
2517 use crate::frontend::FrontendState;
2518 use crate::frontend::api::Version;
2519
2520 let parse_result = Program::parse(kcl_code).map_err(|e| format!("Failed to parse KCL: {}", e))?;
2522 let (program_opt, errors) = parse_result;
2523 if !errors.is_empty() {
2524 return Err(format!("Failed to parse KCL: {:?}", errors));
2525 }
2526 let program = program_opt.ok_or_else(|| "No AST produced".to_string())?;
2527
2528 let mock_ctx = ExecutorContext::new_mock(None).await;
2529
2530 let result = async {
2532 let mut frontend = FrontendState::new();
2533
2534 frontend.program = program.clone();
2536
2537 let exec_outcome = mock_ctx
2538 .run_mock(&program, &MockConfig::default())
2539 .await
2540 .map_err(|e| format!("Failed to execute program: {}", e.error.message()))?;
2541
2542 let exec_outcome = frontend.update_state_after_exec(exec_outcome, false);
2543 #[allow(unused_mut)] let mut initial_scene_graph = frontend.scene_graph.clone();
2545
2546 #[cfg(feature = "artifact-graph")]
2549 if initial_scene_graph.objects.is_empty() && !exec_outcome.scene_objects.is_empty() {
2550 initial_scene_graph.objects = exec_outcome.scene_objects.clone();
2551 }
2552
2553 let actual_sketch_id = if let Some(sketch_mode) = initial_scene_graph.sketch_mode {
2556 sketch_mode
2557 } else {
2558 initial_scene_graph
2560 .objects
2561 .iter()
2562 .find(|obj| matches!(obj.kind, crate::frontend::api::ObjectKind::Sketch { .. }))
2563 .map(|obj| obj.id)
2564 .unwrap_or(sketch_id) };
2566
2567 let version = Version(0);
2568 let initial_scene_graph_delta = crate::frontend::api::SceneGraphDelta {
2569 new_graph: initial_scene_graph,
2570 new_objects: vec![],
2571 invalidates_ids: false,
2572 exec_outcome,
2573 };
2574
2575 let (source_delta, scene_graph_delta) = execute_trim_loop_with_context(
2580 trim_points,
2581 initial_scene_graph_delta,
2582 &mut frontend,
2583 &mock_ctx,
2584 version,
2585 actual_sketch_id,
2586 )
2587 .await?;
2588
2589 if source_delta.text.is_empty() {
2592 return Err("No trim operations were executed - source delta is empty".to_string());
2593 }
2594
2595 Ok(TrimFlowResult {
2596 kcl_code: source_delta.text,
2597 invalidates_ids: scene_graph_delta.invalidates_ids,
2598 })
2599 }
2600 .await;
2601
2602 mock_ctx.close().await;
2604
2605 result
2606}
2607
2608pub async fn execute_trim_loop_with_context(
2614 points: &[Coords2d],
2615 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2616 frontend: &mut crate::frontend::FrontendState,
2617 ctx: &crate::ExecutorContext,
2618 version: crate::frontend::api::Version,
2619 sketch_id: ObjectId,
2620) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
2621 let default_unit = frontend.default_length_unit();
2623 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2624
2625 let mut current_scene_graph_delta = initial_scene_graph_delta.clone();
2628 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2629 crate::frontend::api::SourceDelta { text: String::new() },
2630 initial_scene_graph_delta.clone(),
2631 ));
2632 let mut invalidates_ids = false;
2633 let mut start_index = 0;
2634 let max_iterations = 1000;
2635 let mut iteration_count = 0;
2636 let circle_delete_fallback_strategy =
2637 |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
2638 if !error.contains("No trim termination candidate found for circle") {
2639 return None;
2640 }
2641 let is_circle = scene_objects
2642 .iter()
2643 .find(|obj| obj.id == segment_id)
2644 .is_some_and(|obj| {
2645 matches!(
2646 obj.kind,
2647 ObjectKind::Segment {
2648 segment: Segment::Circle(_)
2649 }
2650 )
2651 });
2652 if is_circle {
2653 Some(vec![TrimOperation::SimpleTrim {
2654 segment_to_trim_id: segment_id,
2655 }])
2656 } else {
2657 None
2658 }
2659 };
2660
2661 let points = normalized_points.as_slice();
2662
2663 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2664 iteration_count += 1;
2665
2666 let next_trim_spawn = get_next_trim_spawn(
2668 points,
2669 start_index,
2670 ¤t_scene_graph_delta.new_graph.objects,
2671 default_unit,
2672 );
2673
2674 match &next_trim_spawn {
2675 TrimItem::None { next_index } => {
2676 let old_start_index = start_index;
2677 start_index = *next_index;
2678 if start_index <= old_start_index {
2679 start_index = old_start_index + 1;
2680 }
2681 if start_index >= points.len().saturating_sub(1) {
2682 break;
2683 }
2684 continue;
2685 }
2686 TrimItem::Spawn {
2687 trim_spawn_seg_id,
2688 trim_spawn_coords,
2689 next_index,
2690 ..
2691 } => {
2692 let terminations = match get_trim_spawn_terminations(
2694 *trim_spawn_seg_id,
2695 points,
2696 ¤t_scene_graph_delta.new_graph.objects,
2697 default_unit,
2698 ) {
2699 Ok(terms) => terms,
2700 Err(e) => {
2701 crate::logln!("Error getting trim spawn terminations: {}", e);
2702 if let Some(strategy) = circle_delete_fallback_strategy(
2703 &e,
2704 *trim_spawn_seg_id,
2705 ¤t_scene_graph_delta.new_graph.objects,
2706 ) {
2707 match execute_trim_operations_simple(
2708 strategy.clone(),
2709 ¤t_scene_graph_delta,
2710 frontend,
2711 ctx,
2712 version,
2713 sketch_id,
2714 )
2715 .await
2716 {
2717 Ok((source_delta, scene_graph_delta)) => {
2718 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2719 last_result = Some((source_delta, scene_graph_delta.clone()));
2720 current_scene_graph_delta = scene_graph_delta;
2721 }
2722 Err(exec_err) => {
2723 crate::logln!(
2724 "Error executing circle-delete fallback trim operation: {}",
2725 exec_err
2726 );
2727 }
2728 }
2729
2730 let old_start_index = start_index;
2731 start_index = *next_index;
2732 if start_index <= old_start_index {
2733 start_index = old_start_index + 1;
2734 }
2735 continue;
2736 }
2737
2738 let old_start_index = start_index;
2739 start_index = *next_index;
2740 if start_index <= old_start_index {
2741 start_index = old_start_index + 1;
2742 }
2743 continue;
2744 }
2745 };
2746
2747 let trim_spawn_segment = current_scene_graph_delta
2749 .new_graph
2750 .objects
2751 .iter()
2752 .find(|obj| obj.id == *trim_spawn_seg_id)
2753 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
2754
2755 let plan = match build_trim_plan(
2756 *trim_spawn_seg_id,
2757 *trim_spawn_coords,
2758 trim_spawn_segment,
2759 &terminations.left_side,
2760 &terminations.right_side,
2761 ¤t_scene_graph_delta.new_graph.objects,
2762 default_unit,
2763 ) {
2764 Ok(plan) => plan,
2765 Err(e) => {
2766 crate::logln!("Error determining trim strategy: {}", e);
2767 let old_start_index = start_index;
2768 start_index = *next_index;
2769 if start_index <= old_start_index {
2770 start_index = old_start_index + 1;
2771 }
2772 continue;
2773 }
2774 };
2775 let strategy = lower_trim_plan(&plan);
2776
2777 let geometry_was_modified = trim_plan_modifies_geometry(&plan);
2780
2781 match execute_trim_operations_simple(
2783 strategy.clone(),
2784 ¤t_scene_graph_delta,
2785 frontend,
2786 ctx,
2787 version,
2788 sketch_id,
2789 )
2790 .await
2791 {
2792 Ok((source_delta, scene_graph_delta)) => {
2793 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2794 last_result = Some((source_delta, scene_graph_delta.clone()));
2795 current_scene_graph_delta = scene_graph_delta;
2796 }
2797 Err(e) => {
2798 crate::logln!("Error executing trim operations: {}", e);
2799 }
2800 }
2801
2802 let old_start_index = start_index;
2804 start_index = *next_index;
2805 if start_index <= old_start_index && !geometry_was_modified {
2806 start_index = old_start_index + 1;
2807 }
2808 }
2809 }
2810 }
2811
2812 if iteration_count >= max_iterations {
2813 return Err(format!("Reached max iterations ({})", max_iterations));
2814 }
2815
2816 let (source_delta, mut scene_graph_delta) =
2817 last_result.ok_or_else(|| "No trim operations were executed".to_string())?;
2818 scene_graph_delta.invalidates_ids = invalidates_ids;
2820 Ok((source_delta, scene_graph_delta))
2821}
2822
2823pub(crate) fn build_trim_plan(
2883 trim_spawn_id: ObjectId,
2884 trim_spawn_coords: Coords2d,
2885 trim_spawn_segment: &Object,
2886 left_side: &TrimTermination,
2887 right_side: &TrimTermination,
2888 objects: &[Object],
2889 default_unit: UnitLength,
2890) -> Result<TrimPlan, String> {
2891 if matches!(left_side, TrimTermination::SegEndPoint { .. })
2893 && matches!(right_side, TrimTermination::SegEndPoint { .. })
2894 {
2895 return Ok(TrimPlan::DeleteSegment {
2896 segment_id: trim_spawn_id,
2897 });
2898 }
2899
2900 let is_intersect_or_coincident = |side: &TrimTermination| -> bool {
2902 matches!(
2903 side,
2904 TrimTermination::Intersection { .. }
2905 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
2906 )
2907 };
2908
2909 let left_side_needs_tail_cut = is_intersect_or_coincident(left_side) && !is_intersect_or_coincident(right_side);
2910 let right_side_needs_tail_cut = is_intersect_or_coincident(right_side) && !is_intersect_or_coincident(left_side);
2911
2912 let ObjectKind::Segment { segment } = &trim_spawn_segment.kind else {
2914 return Err("Trim spawn segment is not a segment".to_string());
2915 };
2916
2917 let (_segment_type, ctor) = match segment {
2918 Segment::Line(line) => ("Line", &line.ctor),
2919 Segment::Arc(arc) => ("Arc", &arc.ctor),
2920 Segment::Circle(circle) => ("Circle", &circle.ctor),
2921 _ => {
2922 return Err("Trim spawn segment is not a Line, Arc, or Circle".to_string());
2923 }
2924 };
2925
2926 let units = match ctor {
2928 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
2929 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2930 _ => NumericSuffix::Mm,
2931 },
2932 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
2933 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2934 _ => NumericSuffix::Mm,
2935 },
2936 SegmentCtor::Circle(circle_ctor) => match &circle_ctor.start.x {
2937 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
2938 _ => NumericSuffix::Mm,
2939 },
2940 _ => NumericSuffix::Mm,
2941 };
2942
2943 let find_distance_constraints_for_segment = |segment_id: ObjectId| -> Vec<ObjectId> {
2945 let mut constraint_ids = Vec::new();
2946 for obj in objects {
2947 let ObjectKind::Constraint { constraint } = &obj.kind else {
2948 continue;
2949 };
2950
2951 let Constraint::Distance(distance) = constraint else {
2952 continue;
2953 };
2954
2955 let points_owned_by_segment: Vec<bool> = distance
2961 .point_ids()
2962 .map(|point_id| {
2963 if let Some(point_obj) = objects.iter().find(|o| o.id == point_id)
2964 && let ObjectKind::Segment { segment } = &point_obj.kind
2965 && let Segment::Point(point) = segment
2966 && let Some(owner_id) = point.owner
2967 {
2968 return owner_id == segment_id;
2969 }
2970 false
2971 })
2972 .collect();
2973
2974 if points_owned_by_segment.len() == 2 && points_owned_by_segment.iter().all(|&owned| owned) {
2976 constraint_ids.push(obj.id);
2977 }
2978 }
2979 constraint_ids
2980 };
2981
2982 let find_existing_point_segment_coincident =
2984 |trim_seg_id: ObjectId, intersecting_seg_id: ObjectId| -> CoincidentData {
2985 let lookup_by_point_id = |point_id: ObjectId| -> Option<CoincidentData> {
2987 for obj in objects {
2988 let ObjectKind::Constraint { constraint } = &obj.kind else {
2989 continue;
2990 };
2991
2992 let Constraint::Coincident(coincident) = constraint else {
2993 continue;
2994 };
2995
2996 let involves_trim_seg = coincident.segment_ids().any(|id| id == trim_seg_id || id == point_id);
2997 let involves_point = coincident.contains_segment(point_id);
2998
2999 if involves_trim_seg && involves_point {
3000 return Some(CoincidentData {
3001 intersecting_seg_id,
3002 intersecting_endpoint_point_id: Some(point_id),
3003 existing_point_segment_constraint_id: Some(obj.id),
3004 });
3005 }
3006 }
3007 None
3008 };
3009
3010 let trim_seg = objects.iter().find(|obj| obj.id == trim_seg_id);
3012
3013 let mut trim_endpoint_ids: Vec<ObjectId> = Vec::new();
3014 if let Some(seg) = trim_seg
3015 && let ObjectKind::Segment { segment } = &seg.kind
3016 {
3017 match segment {
3018 Segment::Line(line) => {
3019 trim_endpoint_ids.push(line.start);
3020 trim_endpoint_ids.push(line.end);
3021 }
3022 Segment::Arc(arc) => {
3023 trim_endpoint_ids.push(arc.start);
3024 trim_endpoint_ids.push(arc.end);
3025 }
3026 _ => {}
3027 }
3028 }
3029
3030 let intersecting_obj = objects.iter().find(|obj| obj.id == intersecting_seg_id);
3031
3032 if let Some(obj) = intersecting_obj
3033 && let ObjectKind::Segment { segment } = &obj.kind
3034 && let Segment::Point(_) = segment
3035 && let Some(found) = lookup_by_point_id(intersecting_seg_id)
3036 {
3037 return found;
3038 }
3039
3040 let mut intersecting_endpoint_ids: Vec<ObjectId> = Vec::new();
3042 if let Some(obj) = intersecting_obj
3043 && let ObjectKind::Segment { segment } = &obj.kind
3044 {
3045 match segment {
3046 Segment::Line(line) => {
3047 intersecting_endpoint_ids.push(line.start);
3048 intersecting_endpoint_ids.push(line.end);
3049 }
3050 Segment::Arc(arc) => {
3051 intersecting_endpoint_ids.push(arc.start);
3052 intersecting_endpoint_ids.push(arc.end);
3053 }
3054 _ => {}
3055 }
3056 }
3057
3058 intersecting_endpoint_ids.push(intersecting_seg_id);
3060
3061 for obj in objects {
3063 let ObjectKind::Constraint { constraint } = &obj.kind else {
3064 continue;
3065 };
3066
3067 let Constraint::Coincident(coincident) = constraint else {
3068 continue;
3069 };
3070
3071 let constraint_segment_ids: Vec<ObjectId> = coincident.get_segments();
3072
3073 let involves_trim_seg = constraint_segment_ids.contains(&trim_seg_id)
3075 || trim_endpoint_ids.iter().any(|&id| constraint_segment_ids.contains(&id));
3076
3077 if !involves_trim_seg {
3078 continue;
3079 }
3080
3081 if let Some(&intersecting_endpoint_id) = intersecting_endpoint_ids
3083 .iter()
3084 .find(|&&id| constraint_segment_ids.contains(&id))
3085 {
3086 return CoincidentData {
3087 intersecting_seg_id,
3088 intersecting_endpoint_point_id: Some(intersecting_endpoint_id),
3089 existing_point_segment_constraint_id: Some(obj.id),
3090 };
3091 }
3092 }
3093
3094 CoincidentData {
3096 intersecting_seg_id,
3097 intersecting_endpoint_point_id: None,
3098 existing_point_segment_constraint_id: None,
3099 }
3100 };
3101
3102 let find_point_segment_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<serde_json::Value> {
3104 let mut constraints: Vec<serde_json::Value> = Vec::new();
3105 for obj in objects {
3106 let ObjectKind::Constraint { constraint } = &obj.kind else {
3107 continue;
3108 };
3109
3110 let Constraint::Coincident(coincident) = constraint else {
3111 continue;
3112 };
3113
3114 if !coincident.contains_segment(endpoint_point_id) {
3116 continue;
3117 }
3118
3119 let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
3121
3122 if let Some(other_id) = other_segment_id
3123 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
3124 {
3125 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
3127 constraints.push(serde_json::json!({
3128 "constraintId": obj.id.0,
3129 "segmentOrPointId": other_id.0,
3130 }));
3131 }
3132 }
3133 }
3134 constraints
3135 };
3136
3137 let find_point_point_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
3140 let mut constraint_ids = Vec::new();
3141 for obj in objects {
3142 let ObjectKind::Constraint { constraint } = &obj.kind else {
3143 continue;
3144 };
3145
3146 let Constraint::Coincident(coincident) = constraint else {
3147 continue;
3148 };
3149
3150 if !coincident.contains_segment(endpoint_point_id) {
3152 continue;
3153 }
3154
3155 let is_point_point = coincident.segment_ids().all(|seg_id| {
3157 if let Some(seg_obj) = objects.iter().find(|o| o.id == seg_id) {
3158 matches!(&seg_obj.kind, ObjectKind::Segment { segment } if matches!(segment, Segment::Point(_)))
3159 } else {
3160 false
3161 }
3162 });
3163
3164 if is_point_point {
3165 constraint_ids.push(obj.id);
3166 }
3167 }
3168 constraint_ids
3169 };
3170
3171 let find_point_segment_coincident_constraint_ids = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
3174 let mut constraint_ids = Vec::new();
3175 for obj in objects {
3176 let ObjectKind::Constraint { constraint } = &obj.kind else {
3177 continue;
3178 };
3179
3180 let Constraint::Coincident(coincident) = constraint else {
3181 continue;
3182 };
3183
3184 if !coincident.contains_segment(endpoint_point_id) {
3186 continue;
3187 }
3188
3189 let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
3191
3192 if let Some(other_id) = other_segment_id
3193 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
3194 {
3195 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
3197 constraint_ids.push(obj.id);
3198 }
3199 }
3200 }
3201 constraint_ids
3202 };
3203
3204 if left_side_needs_tail_cut || right_side_needs_tail_cut {
3206 let side = if left_side_needs_tail_cut {
3207 left_side
3208 } else {
3209 right_side
3210 };
3211
3212 let intersection_coords = match side {
3213 TrimTermination::Intersection {
3214 trim_termination_coords,
3215 ..
3216 }
3217 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3218 trim_termination_coords,
3219 ..
3220 } => *trim_termination_coords,
3221 TrimTermination::SegEndPoint { .. } => {
3222 return Err("Logic error: side should not be segEndPoint here".to_string());
3223 }
3224 };
3225
3226 let endpoint_to_change = if left_side_needs_tail_cut {
3227 EndpointChanged::End
3228 } else {
3229 EndpointChanged::Start
3230 };
3231
3232 let intersecting_seg_id = match side {
3233 TrimTermination::Intersection {
3234 intersecting_seg_id, ..
3235 }
3236 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3237 intersecting_seg_id, ..
3238 } => *intersecting_seg_id,
3239 TrimTermination::SegEndPoint { .. } => {
3240 return Err("Logic error".to_string());
3241 }
3242 };
3243
3244 let mut coincident_data = if matches!(
3245 side,
3246 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3247 ) {
3248 let point_id = match side {
3249 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3250 other_segment_point_id, ..
3251 } => *other_segment_point_id,
3252 _ => return Err("Logic error".to_string()),
3253 };
3254 let mut data = find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id);
3255 data.intersecting_endpoint_point_id = Some(point_id);
3256 data
3257 } else {
3258 find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id)
3259 };
3260
3261 let trim_seg = objects.iter().find(|obj| obj.id == trim_spawn_id);
3263
3264 let endpoint_point_id = if let Some(seg) = trim_seg {
3265 let ObjectKind::Segment { segment } = &seg.kind else {
3266 return Err("Trim spawn segment is not a segment".to_string());
3267 };
3268 match segment {
3269 Segment::Line(line) => {
3270 if endpoint_to_change == EndpointChanged::Start {
3271 Some(line.start)
3272 } else {
3273 Some(line.end)
3274 }
3275 }
3276 Segment::Arc(arc) => {
3277 if endpoint_to_change == EndpointChanged::Start {
3278 Some(arc.start)
3279 } else {
3280 Some(arc.end)
3281 }
3282 }
3283 _ => None,
3284 }
3285 } else {
3286 None
3287 };
3288
3289 if let (Some(endpoint_id), Some(existing_constraint_id)) =
3290 (endpoint_point_id, coincident_data.existing_point_segment_constraint_id)
3291 {
3292 let constraint_involves_trimmed_endpoint = objects
3293 .iter()
3294 .find(|obj| obj.id == existing_constraint_id)
3295 .and_then(|obj| match &obj.kind {
3296 ObjectKind::Constraint {
3297 constraint: Constraint::Coincident(coincident),
3298 } => Some(coincident.contains_segment(endpoint_id) || coincident.contains_segment(trim_spawn_id)),
3299 _ => None,
3300 })
3301 .unwrap_or(false);
3302
3303 if !constraint_involves_trimmed_endpoint {
3304 coincident_data.existing_point_segment_constraint_id = None;
3305 coincident_data.intersecting_endpoint_point_id = None;
3306 }
3307 }
3308
3309 let coincident_end_constraint_to_delete_ids = if let Some(point_id) = endpoint_point_id {
3311 let mut constraint_ids = find_point_point_coincident_constraints(point_id);
3312 constraint_ids.extend(find_point_segment_coincident_constraint_ids(point_id));
3314 constraint_ids
3315 } else {
3316 Vec::new()
3317 };
3318
3319 let point_axis_constraint_ids_to_delete = if let Some(point_id) = endpoint_point_id {
3320 objects
3321 .iter()
3322 .filter_map(|obj| {
3323 let ObjectKind::Constraint { constraint } = &obj.kind else {
3324 return None;
3325 };
3326
3327 point_axis_constraint_references_point(constraint, point_id).then_some(obj.id)
3328 })
3329 .collect::<Vec<_>>()
3330 } else {
3331 Vec::new()
3332 };
3333
3334 let new_ctor = match ctor {
3336 SegmentCtor::Line(line_ctor) => {
3337 let new_point = crate::frontend::sketch::Point2d {
3339 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
3340 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
3341 };
3342 if endpoint_to_change == EndpointChanged::Start {
3343 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3344 start: new_point,
3345 end: line_ctor.end.clone(),
3346 construction: line_ctor.construction,
3347 })
3348 } else {
3349 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3350 start: line_ctor.start.clone(),
3351 end: new_point,
3352 construction: line_ctor.construction,
3353 })
3354 }
3355 }
3356 SegmentCtor::Arc(arc_ctor) => {
3357 let new_point = crate::frontend::sketch::Point2d {
3359 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
3360 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
3361 };
3362 if endpoint_to_change == EndpointChanged::Start {
3363 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3364 start: new_point,
3365 end: arc_ctor.end.clone(),
3366 center: arc_ctor.center.clone(),
3367 construction: arc_ctor.construction,
3368 })
3369 } else {
3370 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3371 start: arc_ctor.start.clone(),
3372 end: new_point,
3373 center: arc_ctor.center.clone(),
3374 construction: arc_ctor.construction,
3375 })
3376 }
3377 }
3378 _ => {
3379 return Err("Unsupported segment type for edit".to_string());
3380 }
3381 };
3382
3383 let mut all_constraint_ids_to_delete: Vec<ObjectId> = Vec::new();
3385 if let Some(constraint_id) = coincident_data.existing_point_segment_constraint_id {
3386 all_constraint_ids_to_delete.push(constraint_id);
3387 }
3388 all_constraint_ids_to_delete.extend(coincident_end_constraint_to_delete_ids);
3389 all_constraint_ids_to_delete.extend(point_axis_constraint_ids_to_delete);
3390
3391 let distance_constraint_ids = find_distance_constraints_for_segment(trim_spawn_id);
3394 all_constraint_ids_to_delete.extend(distance_constraint_ids);
3395
3396 return Ok(TrimPlan::TailCut {
3397 segment_id: trim_spawn_id,
3398 endpoint_changed: endpoint_to_change,
3399 ctor: new_ctor,
3400 segment_or_point_to_make_coincident_to: intersecting_seg_id,
3401 intersecting_endpoint_point_id: coincident_data.intersecting_endpoint_point_id,
3402 constraint_ids_to_delete: all_constraint_ids_to_delete,
3403 });
3404 }
3405
3406 if matches!(segment, Segment::Circle(_)) {
3409 let left_side_intersects = is_intersect_or_coincident(left_side);
3410 let right_side_intersects = is_intersect_or_coincident(right_side);
3411 if !(left_side_intersects && right_side_intersects) {
3412 return Err(format!(
3413 "Unsupported circle trim termination combination: left={:?} right={:?}",
3414 left_side, right_side
3415 ));
3416 }
3417
3418 let left_trim_coords = match left_side {
3419 TrimTermination::SegEndPoint {
3420 trim_termination_coords,
3421 }
3422 | TrimTermination::Intersection {
3423 trim_termination_coords,
3424 ..
3425 }
3426 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3427 trim_termination_coords,
3428 ..
3429 } => *trim_termination_coords,
3430 };
3431 let right_trim_coords = match right_side {
3432 TrimTermination::SegEndPoint {
3433 trim_termination_coords,
3434 }
3435 | TrimTermination::Intersection {
3436 trim_termination_coords,
3437 ..
3438 }
3439 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3440 trim_termination_coords,
3441 ..
3442 } => *trim_termination_coords,
3443 };
3444
3445 let trim_points_coincident = ((left_trim_coords.x - right_trim_coords.x)
3448 * (left_trim_coords.x - right_trim_coords.x)
3449 + (left_trim_coords.y - right_trim_coords.y) * (left_trim_coords.y - right_trim_coords.y))
3450 .sqrt()
3451 <= EPSILON_POINT_ON_SEGMENT * 10.0;
3452 if trim_points_coincident {
3453 return Ok(TrimPlan::DeleteSegment {
3454 segment_id: trim_spawn_id,
3455 });
3456 }
3457
3458 let circle_center_coords =
3459 get_position_coords_from_circle(trim_spawn_segment, CirclePoint::Center, objects, default_unit)
3460 .ok_or_else(|| {
3461 format!(
3462 "Could not get center coordinates for circle segment {}",
3463 trim_spawn_id.0
3464 )
3465 })?;
3466
3467 let spawn_on_left_to_right = is_point_on_arc(
3469 trim_spawn_coords,
3470 circle_center_coords,
3471 left_trim_coords,
3472 right_trim_coords,
3473 EPSILON_POINT_ON_SEGMENT,
3474 );
3475 let (arc_start_coords, arc_end_coords, arc_start_termination, arc_end_termination) = if spawn_on_left_to_right {
3476 (
3477 right_trim_coords,
3478 left_trim_coords,
3479 Box::new(right_side.clone()),
3480 Box::new(left_side.clone()),
3481 )
3482 } else {
3483 (
3484 left_trim_coords,
3485 right_trim_coords,
3486 Box::new(left_side.clone()),
3487 Box::new(right_side.clone()),
3488 )
3489 };
3490
3491 return Ok(TrimPlan::ReplaceCircleWithArc {
3492 circle_id: trim_spawn_id,
3493 arc_start_coords,
3494 arc_end_coords,
3495 arc_start_termination,
3496 arc_end_termination,
3497 });
3498 }
3499
3500 let left_side_intersects = is_intersect_or_coincident(left_side);
3502 let right_side_intersects = is_intersect_or_coincident(right_side);
3503
3504 if left_side_intersects && right_side_intersects {
3505 let left_intersecting_seg_id = match left_side {
3508 TrimTermination::Intersection {
3509 intersecting_seg_id, ..
3510 }
3511 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3512 intersecting_seg_id, ..
3513 } => *intersecting_seg_id,
3514 TrimTermination::SegEndPoint { .. } => {
3515 return Err("Logic error: left side should not be segEndPoint".to_string());
3516 }
3517 };
3518
3519 let right_intersecting_seg_id = match right_side {
3520 TrimTermination::Intersection {
3521 intersecting_seg_id, ..
3522 }
3523 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3524 intersecting_seg_id, ..
3525 } => *intersecting_seg_id,
3526 TrimTermination::SegEndPoint { .. } => {
3527 return Err("Logic error: right side should not be segEndPoint".to_string());
3528 }
3529 };
3530
3531 let left_coincident_data = if matches!(
3532 left_side,
3533 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3534 ) {
3535 let point_id = match left_side {
3536 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3537 other_segment_point_id, ..
3538 } => *other_segment_point_id,
3539 _ => return Err("Logic error".to_string()),
3540 };
3541 let mut data = find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id);
3542 data.intersecting_endpoint_point_id = Some(point_id);
3543 data
3544 } else {
3545 find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id)
3546 };
3547
3548 let right_coincident_data = if matches!(
3549 right_side,
3550 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3551 ) {
3552 let point_id = match right_side {
3553 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3554 other_segment_point_id, ..
3555 } => *other_segment_point_id,
3556 _ => return Err("Logic error".to_string()),
3557 };
3558 let mut data = find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id);
3559 data.intersecting_endpoint_point_id = Some(point_id);
3560 data
3561 } else {
3562 find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id)
3563 };
3564
3565 let (original_start_point_id, original_end_point_id) = match segment {
3567 Segment::Line(line) => (Some(line.start), Some(line.end)),
3568 Segment::Arc(arc) => (Some(arc.start), Some(arc.end)),
3569 _ => (None, None),
3570 };
3571
3572 let original_end_point_coords = match segment {
3574 Segment::Line(_) => {
3575 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
3576 }
3577 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
3578 _ => None,
3579 };
3580
3581 let Some(original_end_coords) = original_end_point_coords else {
3582 return Err(
3583 "Could not get original end point coordinates before editing - this is required for split trim"
3584 .to_string(),
3585 );
3586 };
3587
3588 let left_trim_coords = match left_side {
3590 TrimTermination::SegEndPoint {
3591 trim_termination_coords,
3592 }
3593 | TrimTermination::Intersection {
3594 trim_termination_coords,
3595 ..
3596 }
3597 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3598 trim_termination_coords,
3599 ..
3600 } => *trim_termination_coords,
3601 };
3602
3603 let right_trim_coords = match right_side {
3604 TrimTermination::SegEndPoint {
3605 trim_termination_coords,
3606 }
3607 | TrimTermination::Intersection {
3608 trim_termination_coords,
3609 ..
3610 }
3611 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3612 trim_termination_coords,
3613 ..
3614 } => *trim_termination_coords,
3615 };
3616
3617 let dist_to_original_end = ((right_trim_coords.x - original_end_coords.x)
3619 * (right_trim_coords.x - original_end_coords.x)
3620 + (right_trim_coords.y - original_end_coords.y) * (right_trim_coords.y - original_end_coords.y))
3621 .sqrt();
3622 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3623 return Err(
3624 "Split point is at original end point - this should be handled as cutTail, not split".to_string(),
3625 );
3626 }
3627
3628 let mut constraints_to_migrate: Vec<ConstraintToMigrate> = Vec::new();
3631 let mut constraints_to_delete_set: IndexSet<ObjectId> = IndexSet::new();
3632
3633 if let Some(constraint_id) = left_coincident_data.existing_point_segment_constraint_id {
3635 constraints_to_delete_set.insert(constraint_id);
3636 }
3637 if let Some(constraint_id) = right_coincident_data.existing_point_segment_constraint_id {
3638 constraints_to_delete_set.insert(constraint_id);
3639 }
3640
3641 if let Some(end_id) = original_end_point_id {
3642 for obj in objects {
3643 let ObjectKind::Constraint { constraint } = &obj.kind else {
3644 continue;
3645 };
3646
3647 if point_axis_constraint_references_point(constraint, end_id) {
3648 constraints_to_delete_set.insert(obj.id);
3649 }
3650 }
3651 }
3652
3653 if let Some(end_id) = original_end_point_id {
3655 let end_point_point_constraint_ids = find_point_point_coincident_constraints(end_id);
3656 for constraint_id in end_point_point_constraint_ids {
3657 let other_point_id_opt = objects.iter().find_map(|obj| {
3659 if obj.id != constraint_id {
3660 return None;
3661 }
3662 let ObjectKind::Constraint { constraint } = &obj.kind else {
3663 return None;
3664 };
3665 let Constraint::Coincident(coincident) = constraint else {
3666 return None;
3667 };
3668 coincident.segment_ids().find(|&seg_id| seg_id != end_id)
3669 });
3670
3671 if let Some(other_point_id) = other_point_id_opt {
3672 constraints_to_delete_set.insert(constraint_id);
3673 constraints_to_migrate.push(ConstraintToMigrate {
3675 constraint_id,
3676 other_entity_id: other_point_id,
3677 is_point_point: true,
3678 attach_to_endpoint: AttachToEndpoint::End,
3679 });
3680 }
3681 }
3682 }
3683
3684 if let Some(end_id) = original_end_point_id {
3686 let end_point_segment_constraints = find_point_segment_coincident_constraints(end_id);
3687 for constraint_json in end_point_segment_constraints {
3688 if let Some(constraint_id_usize) = constraint_json
3689 .get("constraintId")
3690 .and_then(|v| v.as_u64())
3691 .map(|id| id as usize)
3692 {
3693 let constraint_id = ObjectId(constraint_id_usize);
3694 constraints_to_delete_set.insert(constraint_id);
3695 if let Some(other_id_usize) = constraint_json
3697 .get("segmentOrPointId")
3698 .and_then(|v| v.as_u64())
3699 .map(|id| id as usize)
3700 {
3701 constraints_to_migrate.push(ConstraintToMigrate {
3702 constraint_id,
3703 other_entity_id: ObjectId(other_id_usize),
3704 is_point_point: false,
3705 attach_to_endpoint: AttachToEndpoint::End,
3706 });
3707 }
3708 }
3709 }
3710 }
3711
3712 if let Some(end_id) = original_end_point_id {
3717 for obj in objects {
3718 let ObjectKind::Constraint { constraint } = &obj.kind else {
3719 continue;
3720 };
3721
3722 let Constraint::Coincident(coincident) = constraint else {
3723 continue;
3724 };
3725
3726 if !coincident.contains_segment(trim_spawn_id) {
3731 continue;
3732 }
3733 if let (Some(start_id), Some(end_id_val)) = (original_start_point_id, Some(end_id))
3736 && coincident.segment_ids().any(|id| id == start_id || id == end_id_val)
3737 {
3738 continue; }
3740
3741 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
3743
3744 if let Some(other_id) = other_id {
3745 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3747 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3748 continue;
3749 };
3750
3751 let Segment::Point(point) = other_segment else {
3752 continue;
3753 };
3754
3755 let point_coords = Coords2d {
3757 x: number_to_unit(&point.position.x, default_unit),
3758 y: number_to_unit(&point.position.y, default_unit),
3759 };
3760
3761 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
3764 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
3765 if let ObjectKind::Segment {
3766 segment: Segment::Point(end_point),
3767 } = &end_point_obj.kind
3768 {
3769 Some(Coords2d {
3770 x: number_to_unit(&end_point.position.x, default_unit),
3771 y: number_to_unit(&end_point.position.y, default_unit),
3772 })
3773 } else {
3774 None
3775 }
3776 } else {
3777 None
3778 }
3779 } else {
3780 None
3781 };
3782
3783 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
3784 let dist_to_original_end = ((point_coords.x - reference_coords.x)
3785 * (point_coords.x - reference_coords.x)
3786 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
3787 .sqrt();
3788
3789 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3790 let has_point_point_constraint = find_point_point_coincident_constraints(end_id)
3793 .iter()
3794 .any(|&constraint_id| {
3795 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
3796 if let ObjectKind::Constraint {
3797 constraint: Constraint::Coincident(coincident),
3798 } = &constraint_obj.kind
3799 {
3800 coincident.contains_segment(other_id)
3801 } else {
3802 false
3803 }
3804 } else {
3805 false
3806 }
3807 });
3808
3809 if !has_point_point_constraint {
3810 constraints_to_migrate.push(ConstraintToMigrate {
3812 constraint_id: obj.id,
3813 other_entity_id: other_id,
3814 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
3817 }
3818 constraints_to_delete_set.insert(obj.id);
3820 }
3821 }
3822 }
3823 }
3824 }
3825
3826 let split_point = right_trim_coords; let segment_start_coords = match segment {
3831 Segment::Line(_) => {
3832 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::Start, objects, default_unit)
3833 }
3834 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Start, objects, default_unit),
3835 _ => None,
3836 };
3837 let segment_end_coords = match segment {
3838 Segment::Line(_) => {
3839 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
3840 }
3841 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
3842 _ => None,
3843 };
3844 let segment_center_coords = match segment {
3845 Segment::Line(_) => None,
3846 Segment::Arc(_) => {
3847 get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Center, objects, default_unit)
3848 }
3849 _ => None,
3850 };
3851
3852 if let (Some(start_coords), Some(end_coords)) = (segment_start_coords, segment_end_coords) {
3853 let split_point_t_opt = match segment {
3855 Segment::Line(_) => Some(project_point_onto_segment(split_point, start_coords, end_coords)),
3856 Segment::Arc(_) => segment_center_coords
3857 .map(|center| project_point_onto_arc(split_point, center, start_coords, end_coords)),
3858 _ => None,
3859 };
3860
3861 if let Some(split_point_t) = split_point_t_opt {
3862 for obj in objects {
3864 let ObjectKind::Constraint { constraint } = &obj.kind else {
3865 continue;
3866 };
3867
3868 let Constraint::Coincident(coincident) = constraint else {
3869 continue;
3870 };
3871
3872 if !coincident.contains_segment(trim_spawn_id) {
3874 continue;
3875 }
3876
3877 if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id)
3879 && coincident.segment_ids().any(|id| id == start_id || id == end_id)
3880 {
3881 continue;
3882 }
3883
3884 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
3886
3887 if let Some(other_id) = other_id {
3888 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3890 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3891 continue;
3892 };
3893
3894 let Segment::Point(point) = other_segment else {
3895 continue;
3896 };
3897
3898 let point_coords = Coords2d {
3900 x: number_to_unit(&point.position.x, default_unit),
3901 y: number_to_unit(&point.position.y, default_unit),
3902 };
3903
3904 let point_t = match segment {
3906 Segment::Line(_) => project_point_onto_segment(point_coords, start_coords, end_coords),
3907 Segment::Arc(_) => {
3908 if let Some(center) = segment_center_coords {
3909 project_point_onto_arc(point_coords, center, start_coords, end_coords)
3910 } else {
3911 continue; }
3913 }
3914 _ => continue, };
3916
3917 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
3920 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
3921 if let ObjectKind::Segment {
3922 segment: Segment::Point(end_point),
3923 } = &end_point_obj.kind
3924 {
3925 Some(Coords2d {
3926 x: number_to_unit(&end_point.position.x, default_unit),
3927 y: number_to_unit(&end_point.position.y, default_unit),
3928 })
3929 } else {
3930 None
3931 }
3932 } else {
3933 None
3934 }
3935 } else {
3936 None
3937 };
3938
3939 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
3940 let dist_to_original_end = ((point_coords.x - reference_coords.x)
3941 * (point_coords.x - reference_coords.x)
3942 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
3943 .sqrt();
3944
3945 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3946 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
3950 find_point_point_coincident_constraints(end_id)
3951 .iter()
3952 .any(|&constraint_id| {
3953 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
3954 {
3955 if let ObjectKind::Constraint {
3956 constraint: Constraint::Coincident(coincident),
3957 } = &constraint_obj.kind
3958 {
3959 coincident.contains_segment(other_id)
3960 } else {
3961 false
3962 }
3963 } else {
3964 false
3965 }
3966 })
3967 } else {
3968 false
3969 };
3970
3971 if !has_point_point_constraint {
3972 constraints_to_migrate.push(ConstraintToMigrate {
3974 constraint_id: obj.id,
3975 other_entity_id: other_id,
3976 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
3979 }
3980 constraints_to_delete_set.insert(obj.id);
3982 continue; }
3984
3985 let dist_to_start = ((point_coords.x - start_coords.x) * (point_coords.x - start_coords.x)
3987 + (point_coords.y - start_coords.y) * (point_coords.y - start_coords.y))
3988 .sqrt();
3989 let is_at_start = (point_t - 0.0).abs() < EPSILON_POINT_ON_SEGMENT
3990 || dist_to_start < EPSILON_POINT_ON_SEGMENT;
3991
3992 if is_at_start {
3993 continue; }
3995
3996 let dist_to_split = (point_t - split_point_t).abs();
3998 if dist_to_split < EPSILON_POINT_ON_SEGMENT * 100.0 {
3999 continue; }
4001
4002 if point_t > split_point_t {
4004 constraints_to_migrate.push(ConstraintToMigrate {
4005 constraint_id: obj.id,
4006 other_entity_id: other_id,
4007 is_point_point: false, attach_to_endpoint: AttachToEndpoint::Segment, });
4010 constraints_to_delete_set.insert(obj.id);
4011 }
4012 }
4013 }
4014 }
4015 } } let distance_constraint_ids_for_split = find_distance_constraints_for_segment(trim_spawn_id);
4023
4024 let arc_center_point_id: Option<ObjectId> = match segment {
4026 Segment::Arc(arc) => Some(arc.center),
4027 _ => None,
4028 };
4029
4030 for constraint_id in distance_constraint_ids_for_split {
4031 if let Some(center_id) = arc_center_point_id {
4033 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
4035 && let ObjectKind::Constraint { constraint } = &constraint_obj.kind
4036 && let Constraint::Distance(distance) = constraint
4037 && distance.contains_point(center_id)
4038 {
4039 continue;
4041 }
4042 }
4043
4044 constraints_to_delete_set.insert(constraint_id);
4045 }
4046
4047 for obj in objects {
4055 let ObjectKind::Constraint { constraint } = &obj.kind else {
4056 continue;
4057 };
4058
4059 let Constraint::Coincident(coincident) = constraint else {
4060 continue;
4061 };
4062
4063 if !coincident.contains_segment(trim_spawn_id) {
4065 continue;
4066 }
4067
4068 if constraints_to_delete_set.contains(&obj.id) {
4070 continue;
4071 }
4072
4073 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
4080
4081 if let Some(other_id) = other_id {
4082 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
4084 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
4085 continue;
4086 };
4087
4088 let Segment::Point(point) = other_segment else {
4089 continue;
4090 };
4091
4092 let _is_endpoint_constraint =
4095 if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id) {
4096 coincident.segment_ids().any(|id| id == start_id || id == end_id)
4097 } else {
4098 false
4099 };
4100
4101 let point_coords = Coords2d {
4103 x: number_to_unit(&point.position.x, default_unit),
4104 y: number_to_unit(&point.position.y, default_unit),
4105 };
4106
4107 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
4109 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
4110 if let ObjectKind::Segment {
4111 segment: Segment::Point(end_point),
4112 } = &end_point_obj.kind
4113 {
4114 Some(Coords2d {
4115 x: number_to_unit(&end_point.position.x, default_unit),
4116 y: number_to_unit(&end_point.position.y, default_unit),
4117 })
4118 } else {
4119 None
4120 }
4121 } else {
4122 None
4123 }
4124 } else {
4125 None
4126 };
4127
4128 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
4129 let dist_to_original_end = ((point_coords.x - reference_coords.x)
4130 * (point_coords.x - reference_coords.x)
4131 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
4132 .sqrt();
4133
4134 let is_at_original_end = dist_to_original_end < EPSILON_POINT_ON_SEGMENT * 2.0;
4137
4138 if is_at_original_end {
4139 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
4142 find_point_point_coincident_constraints(end_id)
4143 .iter()
4144 .any(|&constraint_id| {
4145 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
4146 if let ObjectKind::Constraint {
4147 constraint: Constraint::Coincident(coincident),
4148 } = &constraint_obj.kind
4149 {
4150 coincident.contains_segment(other_id)
4151 } else {
4152 false
4153 }
4154 } else {
4155 false
4156 }
4157 })
4158 } else {
4159 false
4160 };
4161
4162 if !has_point_point_constraint {
4163 constraints_to_migrate.push(ConstraintToMigrate {
4165 constraint_id: obj.id,
4166 other_entity_id: other_id,
4167 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
4170 }
4171 constraints_to_delete_set.insert(obj.id);
4173 }
4174 }
4175 }
4176 }
4177
4178 let constraints_to_delete: Vec<ObjectId> = constraints_to_delete_set.iter().copied().collect();
4180 let plan = TrimPlan::SplitSegment {
4181 segment_id: trim_spawn_id,
4182 left_trim_coords,
4183 right_trim_coords,
4184 original_end_coords,
4185 left_side: Box::new(left_side.clone()),
4186 right_side: Box::new(right_side.clone()),
4187 left_side_coincident_data: CoincidentData {
4188 intersecting_seg_id: left_intersecting_seg_id,
4189 intersecting_endpoint_point_id: left_coincident_data.intersecting_endpoint_point_id,
4190 existing_point_segment_constraint_id: left_coincident_data.existing_point_segment_constraint_id,
4191 },
4192 right_side_coincident_data: CoincidentData {
4193 intersecting_seg_id: right_intersecting_seg_id,
4194 intersecting_endpoint_point_id: right_coincident_data.intersecting_endpoint_point_id,
4195 existing_point_segment_constraint_id: right_coincident_data.existing_point_segment_constraint_id,
4196 },
4197 constraints_to_migrate,
4198 constraints_to_delete,
4199 };
4200
4201 return Ok(plan);
4202 }
4203
4204 Err(format!(
4209 "Unsupported trim termination combination: left={:?} right={:?}",
4210 left_side, right_side
4211 ))
4212}
4213
4214pub(crate) async fn execute_trim_operations_simple(
4226 strategy: Vec<TrimOperation>,
4227 current_scene_graph_delta: &crate::frontend::api::SceneGraphDelta,
4228 frontend: &mut crate::frontend::FrontendState,
4229 ctx: &crate::ExecutorContext,
4230 version: crate::frontend::api::Version,
4231 sketch_id: ObjectId,
4232) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
4233 use crate::frontend::SketchApi;
4234 use crate::frontend::sketch::Constraint;
4235 use crate::frontend::sketch::ExistingSegmentCtor;
4236 use crate::frontend::sketch::SegmentCtor;
4237
4238 let default_unit = frontend.default_length_unit();
4239
4240 let mut op_index = 0;
4241 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = None;
4242 let mut invalidates_ids = false;
4243
4244 while op_index < strategy.len() {
4245 let mut consumed_ops = 1;
4246 let operation_result = match &strategy[op_index] {
4247 TrimOperation::SimpleTrim { segment_to_trim_id } => {
4248 frontend
4250 .delete_objects(
4251 ctx,
4252 version,
4253 sketch_id,
4254 Vec::new(), vec![*segment_to_trim_id], )
4257 .await
4258 .map_err(|e| format!("Failed to delete segment: {}", e.error.message()))
4259 }
4260 TrimOperation::EditSegment {
4261 segment_id,
4262 ctor,
4263 endpoint_changed,
4264 } => {
4265 if op_index + 1 < strategy.len() {
4268 if let TrimOperation::AddCoincidentConstraint {
4269 segment_id: coincident_seg_id,
4270 endpoint_changed: coincident_endpoint_changed,
4271 segment_or_point_to_make_coincident_to,
4272 intersecting_endpoint_point_id,
4273 } = &strategy[op_index + 1]
4274 {
4275 if segment_id == coincident_seg_id && endpoint_changed == coincident_endpoint_changed {
4276 let mut delete_constraint_ids: Vec<ObjectId> = Vec::new();
4278 consumed_ops = 2;
4279
4280 if op_index + 2 < strategy.len()
4281 && let TrimOperation::DeleteConstraints { constraint_ids } = &strategy[op_index + 2]
4282 {
4283 delete_constraint_ids = constraint_ids.to_vec();
4284 consumed_ops = 3;
4285 }
4286
4287 let segment_ctor = ctor.clone();
4289
4290 let edited_segment = current_scene_graph_delta
4292 .new_graph
4293 .objects
4294 .iter()
4295 .find(|obj| obj.id == *segment_id)
4296 .ok_or_else(|| format!("Failed to find segment {} for tail-cut batch", segment_id.0))?;
4297
4298 let endpoint_point_id = match &edited_segment.kind {
4299 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4300 crate::frontend::sketch::Segment::Line(line) => {
4301 if *endpoint_changed == EndpointChanged::Start {
4302 line.start
4303 } else {
4304 line.end
4305 }
4306 }
4307 crate::frontend::sketch::Segment::Arc(arc) => {
4308 if *endpoint_changed == EndpointChanged::Start {
4309 arc.start
4310 } else {
4311 arc.end
4312 }
4313 }
4314 _ => {
4315 return Err("Unsupported segment type for tail-cut batch".to_string());
4316 }
4317 },
4318 _ => {
4319 return Err("Edited object is not a segment (tail-cut batch)".to_string());
4320 }
4321 };
4322
4323 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
4324 vec![endpoint_point_id.into(), (*point_id).into()]
4325 } else {
4326 vec![
4327 endpoint_point_id.into(),
4328 (*segment_or_point_to_make_coincident_to).into(),
4329 ]
4330 };
4331
4332 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4333 segments: coincident_segments,
4334 });
4335
4336 let segment_to_edit = ExistingSegmentCtor {
4337 id: *segment_id,
4338 ctor: segment_ctor,
4339 };
4340
4341 frontend
4344 .batch_tail_cut_operations(
4345 ctx,
4346 version,
4347 sketch_id,
4348 vec![segment_to_edit],
4349 vec![constraint],
4350 delete_constraint_ids,
4351 )
4352 .await
4353 .map_err(|e| format!("Failed to batch tail-cut operations: {}", e.error.message()))
4354 } else {
4355 let segment_to_edit = ExistingSegmentCtor {
4357 id: *segment_id,
4358 ctor: ctor.clone(),
4359 };
4360
4361 frontend
4362 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4363 .await
4364 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4365 }
4366 } else {
4367 let segment_to_edit = ExistingSegmentCtor {
4369 id: *segment_id,
4370 ctor: ctor.clone(),
4371 };
4372
4373 frontend
4374 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4375 .await
4376 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4377 }
4378 } else {
4379 let segment_to_edit = ExistingSegmentCtor {
4381 id: *segment_id,
4382 ctor: ctor.clone(),
4383 };
4384
4385 frontend
4386 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4387 .await
4388 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4389 }
4390 }
4391 TrimOperation::AddCoincidentConstraint {
4392 segment_id,
4393 endpoint_changed,
4394 segment_or_point_to_make_coincident_to,
4395 intersecting_endpoint_point_id,
4396 } => {
4397 let edited_segment = current_scene_graph_delta
4399 .new_graph
4400 .objects
4401 .iter()
4402 .find(|obj| obj.id == *segment_id)
4403 .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
4404
4405 let new_segment_endpoint_point_id = match &edited_segment.kind {
4407 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4408 crate::frontend::sketch::Segment::Line(line) => {
4409 if *endpoint_changed == EndpointChanged::Start {
4410 line.start
4411 } else {
4412 line.end
4413 }
4414 }
4415 crate::frontend::sketch::Segment::Arc(arc) => {
4416 if *endpoint_changed == EndpointChanged::Start {
4417 arc.start
4418 } else {
4419 arc.end
4420 }
4421 }
4422 _ => {
4423 return Err("Unsupported segment type for addCoincidentConstraint".to_string());
4424 }
4425 },
4426 _ => {
4427 return Err("Edited object is not a segment".to_string());
4428 }
4429 };
4430
4431 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
4433 vec![new_segment_endpoint_point_id.into(), (*point_id).into()]
4434 } else {
4435 vec![
4436 new_segment_endpoint_point_id.into(),
4437 (*segment_or_point_to_make_coincident_to).into(),
4438 ]
4439 };
4440
4441 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4442 segments: coincident_segments,
4443 });
4444
4445 frontend
4446 .add_constraint(ctx, version, sketch_id, constraint)
4447 .await
4448 .map_err(|e| format!("Failed to add constraint: {}", e.error.message()))
4449 }
4450 TrimOperation::DeleteConstraints { constraint_ids } => {
4451 let constraint_object_ids: Vec<ObjectId> = constraint_ids.to_vec();
4453
4454 frontend
4455 .delete_objects(
4456 ctx,
4457 version,
4458 sketch_id,
4459 constraint_object_ids,
4460 Vec::new(), )
4462 .await
4463 .map_err(|e| format!("Failed to delete constraints: {}", e.error.message()))
4464 }
4465 TrimOperation::ReplaceCircleWithArc {
4466 circle_id,
4467 arc_start_coords,
4468 arc_end_coords,
4469 arc_start_termination,
4470 arc_end_termination,
4471 } => {
4472 let original_circle = current_scene_graph_delta
4474 .new_graph
4475 .objects
4476 .iter()
4477 .find(|obj| obj.id == *circle_id)
4478 .ok_or_else(|| format!("Failed to find original circle {}", circle_id.0))?;
4479
4480 let (original_circle_start_id, original_circle_center_id, circle_ctor) = match &original_circle.kind {
4481 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4482 crate::frontend::sketch::Segment::Circle(circle) => match &circle.ctor {
4483 SegmentCtor::Circle(circle_ctor) => (circle.start, circle.center, circle_ctor.clone()),
4484 _ => return Err("Circle does not have a Circle ctor".to_string()),
4485 },
4486 _ => return Err("Original segment is not a circle".to_string()),
4487 },
4488 _ => return Err("Original object is not a segment".to_string()),
4489 };
4490
4491 let units = match &circle_ctor.start.x {
4492 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4493 _ => crate::pretty::NumericSuffix::Mm,
4494 };
4495
4496 let coords_to_point_expr = |coords: Coords2d| crate::frontend::sketch::Point2d {
4497 x: crate::frontend::api::Expr::Var(unit_to_number(coords.x, default_unit, units)),
4498 y: crate::frontend::api::Expr::Var(unit_to_number(coords.y, default_unit, units)),
4499 };
4500
4501 let arc_ctor = SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4502 start: coords_to_point_expr(*arc_start_coords),
4503 end: coords_to_point_expr(*arc_end_coords),
4504 center: circle_ctor.center.clone(),
4505 construction: circle_ctor.construction,
4506 });
4507
4508 let (_add_source_delta, add_scene_graph_delta) = frontend
4509 .add_segment(ctx, version, sketch_id, arc_ctor, None)
4510 .await
4511 .map_err(|e| format!("Failed to add arc while replacing circle: {}", e.error.message()))?;
4512 invalidates_ids = invalidates_ids || add_scene_graph_delta.invalidates_ids;
4513
4514 let new_arc_id = *add_scene_graph_delta
4515 .new_objects
4516 .iter()
4517 .find(|&id| {
4518 add_scene_graph_delta
4519 .new_graph
4520 .objects
4521 .iter()
4522 .find(|o| o.id == *id)
4523 .is_some_and(|obj| {
4524 matches!(
4525 &obj.kind,
4526 crate::frontend::api::ObjectKind::Segment { segment }
4527 if matches!(segment, crate::frontend::sketch::Segment::Arc(_))
4528 )
4529 })
4530 })
4531 .ok_or_else(|| "Failed to find newly created arc segment".to_string())?;
4532
4533 let new_arc_obj = add_scene_graph_delta
4534 .new_graph
4535 .objects
4536 .iter()
4537 .find(|obj| obj.id == new_arc_id)
4538 .ok_or_else(|| format!("New arc segment not found {}", new_arc_id.0))?;
4539 let (new_arc_start_id, new_arc_end_id, new_arc_center_id) = match &new_arc_obj.kind {
4540 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4541 crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, arc.center),
4542 _ => return Err("New segment is not an arc".to_string()),
4543 },
4544 _ => return Err("New arc object is not a segment".to_string()),
4545 };
4546
4547 let constraint_segments_for =
4548 |arc_endpoint_id: ObjectId,
4549 term: &TrimTermination|
4550 -> Result<Vec<crate::frontend::sketch::ConstraintSegment>, String> {
4551 match term {
4552 TrimTermination::Intersection {
4553 intersecting_seg_id, ..
4554 } => Ok(vec![arc_endpoint_id.into(), (*intersecting_seg_id).into()]),
4555 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4556 other_segment_point_id,
4557 ..
4558 } => Ok(vec![arc_endpoint_id.into(), (*other_segment_point_id).into()]),
4559 TrimTermination::SegEndPoint { .. } => {
4560 Err("Circle replacement endpoint cannot terminate at seg endpoint".to_string())
4561 }
4562 }
4563 };
4564
4565 let start_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4566 segments: constraint_segments_for(new_arc_start_id, arc_start_termination)?,
4567 });
4568 let (_c1_source_delta, c1_scene_graph_delta) = frontend
4569 .add_constraint(ctx, version, sketch_id, start_constraint)
4570 .await
4571 .map_err(|e| format!("Failed to add start coincident on replaced arc: {}", e.error.message()))?;
4572 invalidates_ids = invalidates_ids || c1_scene_graph_delta.invalidates_ids;
4573
4574 let end_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4575 segments: constraint_segments_for(new_arc_end_id, arc_end_termination)?,
4576 });
4577 let (_c2_source_delta, c2_scene_graph_delta) = frontend
4578 .add_constraint(ctx, version, sketch_id, end_constraint)
4579 .await
4580 .map_err(|e| format!("Failed to add end coincident on replaced arc: {}", e.error.message()))?;
4581 invalidates_ids = invalidates_ids || c2_scene_graph_delta.invalidates_ids;
4582
4583 let mut termination_point_ids: Vec<ObjectId> = Vec::new();
4584 for term in [arc_start_termination, arc_end_termination] {
4585 if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4586 other_segment_point_id,
4587 ..
4588 } = term.as_ref()
4589 {
4590 termination_point_ids.push(*other_segment_point_id);
4591 }
4592 }
4593
4594 let rewrite_map = std::collections::HashMap::from([
4598 (*circle_id, new_arc_id),
4599 (original_circle_center_id, new_arc_center_id),
4600 (original_circle_start_id, new_arc_start_id),
4601 ]);
4602 let rewrite_ids: std::collections::HashSet<ObjectId> = rewrite_map.keys().copied().collect();
4603
4604 let mut migrated_constraints: Vec<Constraint> = Vec::new();
4605 for obj in ¤t_scene_graph_delta.new_graph.objects {
4606 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4607 continue;
4608 };
4609
4610 match constraint {
4611 Constraint::Coincident(coincident) => {
4612 if !constraint_segments_reference_any(&coincident.segments, &rewrite_ids) {
4613 continue;
4614 }
4615
4616 if coincident.contains_segment(*circle_id)
4620 && coincident
4621 .segment_ids()
4622 .filter(|id| *id != *circle_id)
4623 .any(|id| termination_point_ids.contains(&id))
4624 {
4625 continue;
4626 }
4627
4628 let Some(Constraint::Coincident(migrated_coincident)) =
4629 rewrite_constraint_with_map(constraint, &rewrite_map)
4630 else {
4631 continue;
4632 };
4633
4634 let migrated_ids: Vec<ObjectId> = migrated_coincident
4638 .segments
4639 .iter()
4640 .filter_map(|segment| match segment {
4641 crate::frontend::sketch::ConstraintSegment::Segment(id) => Some(*id),
4642 crate::frontend::sketch::ConstraintSegment::Origin(_) => None,
4643 })
4644 .collect();
4645 if migrated_ids.contains(&new_arc_id)
4646 && (migrated_ids.contains(&new_arc_start_id) || migrated_ids.contains(&new_arc_end_id))
4647 {
4648 continue;
4649 }
4650
4651 migrated_constraints.push(Constraint::Coincident(migrated_coincident));
4652 }
4653 Constraint::Distance(distance) => {
4654 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4655 continue;
4656 }
4657 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4658 migrated_constraints.push(migrated);
4659 }
4660 }
4661 Constraint::HorizontalDistance(distance) => {
4662 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4663 continue;
4664 }
4665 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4666 migrated_constraints.push(migrated);
4667 }
4668 }
4669 Constraint::VerticalDistance(distance) => {
4670 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4671 continue;
4672 }
4673 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4674 migrated_constraints.push(migrated);
4675 }
4676 }
4677 Constraint::Radius(radius) => {
4678 if radius.arc == *circle_id
4679 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4680 {
4681 migrated_constraints.push(migrated);
4682 }
4683 }
4684 Constraint::Diameter(diameter) => {
4685 if diameter.arc == *circle_id
4686 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4687 {
4688 migrated_constraints.push(migrated);
4689 }
4690 }
4691 Constraint::EqualRadius(equal_radius) => {
4692 if equal_radius.input.contains(circle_id)
4693 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4694 {
4695 migrated_constraints.push(migrated);
4696 }
4697 }
4698 Constraint::Tangent(tangent) => {
4699 if tangent.input.contains(circle_id)
4700 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4701 {
4702 migrated_constraints.push(migrated);
4703 }
4704 }
4705 _ => {}
4706 }
4707 }
4708
4709 for constraint in migrated_constraints {
4710 let (_source_delta, migrated_scene_graph_delta) = frontend
4711 .add_constraint(ctx, version, sketch_id, constraint)
4712 .await
4713 .map_err(|e| format!("Failed to migrate circle constraint to arc: {}", e.error.message()))?;
4714 invalidates_ids = invalidates_ids || migrated_scene_graph_delta.invalidates_ids;
4715 }
4716
4717 frontend
4718 .delete_objects(ctx, version, sketch_id, Vec::new(), vec![*circle_id])
4719 .await
4720 .map_err(|e| format!("Failed to delete circle after arc replacement: {}", e.error.message()))
4721 }
4722 TrimOperation::SplitSegment {
4723 segment_id,
4724 left_trim_coords,
4725 right_trim_coords,
4726 original_end_coords,
4727 left_side,
4728 right_side,
4729 constraints_to_migrate,
4730 constraints_to_delete,
4731 ..
4732 } => {
4733 let original_segment = current_scene_graph_delta
4738 .new_graph
4739 .objects
4740 .iter()
4741 .find(|obj| obj.id == *segment_id)
4742 .ok_or_else(|| format!("Failed to find original segment {}", segment_id.0))?;
4743
4744 let (original_segment_start_point_id, original_segment_end_point_id, original_segment_center_point_id) =
4746 match &original_segment.kind {
4747 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4748 crate::frontend::sketch::Segment::Line(line) => (Some(line.start), Some(line.end), None),
4749 crate::frontend::sketch::Segment::Arc(arc) => {
4750 (Some(arc.start), Some(arc.end), Some(arc.center))
4751 }
4752 _ => (None, None, None),
4753 },
4754 _ => (None, None, None),
4755 };
4756
4757 let mut center_point_constraints_to_migrate: Vec<(Constraint, ObjectId)> = Vec::new();
4759 if let Some(original_center_id) = original_segment_center_point_id {
4760 for obj in ¤t_scene_graph_delta.new_graph.objects {
4761 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4762 continue;
4763 };
4764
4765 if let Constraint::Coincident(coincident) = constraint
4767 && coincident.contains_segment(original_center_id)
4768 {
4769 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
4770 }
4771
4772 if let Constraint::Distance(distance) = constraint
4774 && distance.contains_point(original_center_id)
4775 {
4776 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
4777 }
4778 }
4779 }
4780
4781 let (_segment_type, original_ctor) = match &original_segment.kind {
4783 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4784 crate::frontend::sketch::Segment::Line(line) => ("Line", line.ctor.clone()),
4785 crate::frontend::sketch::Segment::Arc(arc) => ("Arc", arc.ctor.clone()),
4786 _ => {
4787 return Err("Original segment is not a Line or Arc".to_string());
4788 }
4789 },
4790 _ => {
4791 return Err("Original object is not a segment".to_string());
4792 }
4793 };
4794
4795 let units = match &original_ctor {
4797 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
4798 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4799 _ => crate::pretty::NumericSuffix::Mm,
4800 },
4801 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
4802 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4803 _ => crate::pretty::NumericSuffix::Mm,
4804 },
4805 _ => crate::pretty::NumericSuffix::Mm,
4806 };
4807
4808 let coords_to_point =
4811 |coords: Coords2d| -> crate::frontend::sketch::Point2d<crate::frontend::api::Number> {
4812 crate::frontend::sketch::Point2d {
4813 x: unit_to_number(coords.x, default_unit, units),
4814 y: unit_to_number(coords.y, default_unit, units),
4815 }
4816 };
4817
4818 let point_to_expr = |point: crate::frontend::sketch::Point2d<crate::frontend::api::Number>| -> crate::frontend::sketch::Point2d<crate::frontend::api::Expr> {
4820 crate::frontend::sketch::Point2d {
4821 x: crate::frontend::api::Expr::Var(point.x),
4822 y: crate::frontend::api::Expr::Var(point.y),
4823 }
4824 };
4825
4826 let new_segment_ctor = match &original_ctor {
4828 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
4829 start: point_to_expr(coords_to_point(*right_trim_coords)),
4830 end: point_to_expr(coords_to_point(*original_end_coords)),
4831 construction: line_ctor.construction,
4832 }),
4833 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4834 start: point_to_expr(coords_to_point(*right_trim_coords)),
4835 end: point_to_expr(coords_to_point(*original_end_coords)),
4836 center: arc_ctor.center.clone(),
4837 construction: arc_ctor.construction,
4838 }),
4839 _ => {
4840 return Err("Unsupported segment type for new segment".to_string());
4841 }
4842 };
4843
4844 let (_add_source_delta, add_scene_graph_delta) = frontend
4845 .add_segment(ctx, version, sketch_id, new_segment_ctor, None)
4846 .await
4847 .map_err(|e| format!("Failed to add new segment: {}", e.error.message()))?;
4848
4849 let new_segment_id = *add_scene_graph_delta
4851 .new_objects
4852 .iter()
4853 .find(|&id| {
4854 if let Some(obj) = add_scene_graph_delta.new_graph.objects.iter().find(|o| o.id == *id) {
4855 matches!(
4856 &obj.kind,
4857 crate::frontend::api::ObjectKind::Segment { segment }
4858 if matches!(segment, crate::frontend::sketch::Segment::Line(_) | crate::frontend::sketch::Segment::Arc(_))
4859 )
4860 } else {
4861 false
4862 }
4863 })
4864 .ok_or_else(|| "Failed to find newly created segment".to_string())?;
4865
4866 let new_segment = add_scene_graph_delta
4867 .new_graph
4868 .objects
4869 .iter()
4870 .find(|o| o.id == new_segment_id)
4871 .ok_or_else(|| format!("New segment not found with id {}", new_segment_id.0))?;
4872
4873 let (new_segment_start_point_id, new_segment_end_point_id, new_segment_center_point_id) =
4875 match &new_segment.kind {
4876 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4877 crate::frontend::sketch::Segment::Line(line) => (line.start, line.end, None),
4878 crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, Some(arc.center)),
4879 _ => {
4880 return Err("New segment is not a Line or Arc".to_string());
4881 }
4882 },
4883 _ => {
4884 return Err("New segment is not a segment".to_string());
4885 }
4886 };
4887
4888 let edited_ctor = match &original_ctor {
4890 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
4891 start: line_ctor.start.clone(),
4892 end: point_to_expr(coords_to_point(*left_trim_coords)),
4893 construction: line_ctor.construction,
4894 }),
4895 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4896 start: arc_ctor.start.clone(),
4897 end: point_to_expr(coords_to_point(*left_trim_coords)),
4898 center: arc_ctor.center.clone(),
4899 construction: arc_ctor.construction,
4900 }),
4901 _ => {
4902 return Err("Unsupported segment type for split".to_string());
4903 }
4904 };
4905
4906 let (_edit_source_delta, edit_scene_graph_delta) = frontend
4907 .edit_segments(
4908 ctx,
4909 version,
4910 sketch_id,
4911 vec![ExistingSegmentCtor {
4912 id: *segment_id,
4913 ctor: edited_ctor,
4914 }],
4915 )
4916 .await
4917 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))?;
4918 invalidates_ids = invalidates_ids || edit_scene_graph_delta.invalidates_ids;
4920
4921 let edited_segment = edit_scene_graph_delta
4923 .new_graph
4924 .objects
4925 .iter()
4926 .find(|obj| obj.id == *segment_id)
4927 .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
4928
4929 let left_side_endpoint_point_id = match &edited_segment.kind {
4930 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4931 crate::frontend::sketch::Segment::Line(line) => line.end,
4932 crate::frontend::sketch::Segment::Arc(arc) => arc.end,
4933 _ => {
4934 return Err("Edited segment is not a Line or Arc".to_string());
4935 }
4936 },
4937 _ => {
4938 return Err("Edited segment is not a segment".to_string());
4939 }
4940 };
4941
4942 let mut batch_constraints = Vec::new();
4944
4945 let left_intersecting_seg_id = match &**left_side {
4947 TrimTermination::Intersection {
4948 intersecting_seg_id, ..
4949 }
4950 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4951 intersecting_seg_id, ..
4952 } => *intersecting_seg_id,
4953 _ => {
4954 return Err("Left side is not an intersection or coincident".to_string());
4955 }
4956 };
4957 let left_coincident_segments = match &**left_side {
4958 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4959 other_segment_point_id,
4960 ..
4961 } => {
4962 vec![left_side_endpoint_point_id.into(), (*other_segment_point_id).into()]
4963 }
4964 _ => {
4965 vec![left_side_endpoint_point_id.into(), left_intersecting_seg_id.into()]
4966 }
4967 };
4968 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
4969 segments: left_coincident_segments,
4970 }));
4971
4972 let right_intersecting_seg_id = match &**right_side {
4974 TrimTermination::Intersection {
4975 intersecting_seg_id, ..
4976 }
4977 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4978 intersecting_seg_id, ..
4979 } => *intersecting_seg_id,
4980 _ => {
4981 return Err("Right side is not an intersection or coincident".to_string());
4982 }
4983 };
4984
4985 let mut intersection_point_id: Option<ObjectId> = None;
4986 if matches!(&**right_side, TrimTermination::Intersection { .. }) {
4987 let intersecting_seg = edit_scene_graph_delta
4988 .new_graph
4989 .objects
4990 .iter()
4991 .find(|obj| obj.id == right_intersecting_seg_id);
4992
4993 if let Some(seg) = intersecting_seg {
4994 let endpoint_epsilon = 1e-3; let right_trim_coords_value = *right_trim_coords;
4996
4997 if let crate::frontend::api::ObjectKind::Segment { segment } = &seg.kind {
4998 match segment {
4999 crate::frontend::sketch::Segment::Line(_) => {
5000 if let (Some(start_coords), Some(end_coords)) = (
5001 crate::frontend::trim::get_position_coords_for_line(
5002 seg,
5003 crate::frontend::trim::LineEndpoint::Start,
5004 &edit_scene_graph_delta.new_graph.objects,
5005 default_unit,
5006 ),
5007 crate::frontend::trim::get_position_coords_for_line(
5008 seg,
5009 crate::frontend::trim::LineEndpoint::End,
5010 &edit_scene_graph_delta.new_graph.objects,
5011 default_unit,
5012 ),
5013 ) {
5014 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
5015 * (right_trim_coords_value.x - start_coords.x)
5016 + (right_trim_coords_value.y - start_coords.y)
5017 * (right_trim_coords_value.y - start_coords.y))
5018 .sqrt();
5019 if dist_to_start < endpoint_epsilon {
5020 if let crate::frontend::sketch::Segment::Line(line) = segment {
5021 intersection_point_id = Some(line.start);
5022 }
5023 } else {
5024 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
5025 * (right_trim_coords_value.x - end_coords.x)
5026 + (right_trim_coords_value.y - end_coords.y)
5027 * (right_trim_coords_value.y - end_coords.y))
5028 .sqrt();
5029 if dist_to_end < endpoint_epsilon
5030 && let crate::frontend::sketch::Segment::Line(line) = segment
5031 {
5032 intersection_point_id = Some(line.end);
5033 }
5034 }
5035 }
5036 }
5037 crate::frontend::sketch::Segment::Arc(_) => {
5038 if let (Some(start_coords), Some(end_coords)) = (
5039 crate::frontend::trim::get_position_coords_from_arc(
5040 seg,
5041 crate::frontend::trim::ArcPoint::Start,
5042 &edit_scene_graph_delta.new_graph.objects,
5043 default_unit,
5044 ),
5045 crate::frontend::trim::get_position_coords_from_arc(
5046 seg,
5047 crate::frontend::trim::ArcPoint::End,
5048 &edit_scene_graph_delta.new_graph.objects,
5049 default_unit,
5050 ),
5051 ) {
5052 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
5053 * (right_trim_coords_value.x - start_coords.x)
5054 + (right_trim_coords_value.y - start_coords.y)
5055 * (right_trim_coords_value.y - start_coords.y))
5056 .sqrt();
5057 if dist_to_start < endpoint_epsilon {
5058 if let crate::frontend::sketch::Segment::Arc(arc) = segment {
5059 intersection_point_id = Some(arc.start);
5060 }
5061 } else {
5062 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
5063 * (right_trim_coords_value.x - end_coords.x)
5064 + (right_trim_coords_value.y - end_coords.y)
5065 * (right_trim_coords_value.y - end_coords.y))
5066 .sqrt();
5067 if dist_to_end < endpoint_epsilon
5068 && let crate::frontend::sketch::Segment::Arc(arc) = segment
5069 {
5070 intersection_point_id = Some(arc.end);
5071 }
5072 }
5073 }
5074 }
5075 _ => {}
5076 }
5077 }
5078 }
5079 }
5080
5081 let right_coincident_segments = if let Some(point_id) = intersection_point_id {
5082 vec![new_segment_start_point_id.into(), point_id.into()]
5083 } else if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5084 other_segment_point_id,
5085 ..
5086 } = &**right_side
5087 {
5088 vec![new_segment_start_point_id.into(), (*other_segment_point_id).into()]
5089 } else {
5090 vec![new_segment_start_point_id.into(), right_intersecting_seg_id.into()]
5091 };
5092 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
5093 segments: right_coincident_segments,
5094 }));
5095
5096 let mut points_constrained_to_new_segment_start = std::collections::HashSet::new();
5098 let mut points_constrained_to_new_segment_end = std::collections::HashSet::new();
5099
5100 if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5101 other_segment_point_id,
5102 ..
5103 } = &**right_side
5104 {
5105 points_constrained_to_new_segment_start.insert(other_segment_point_id);
5106 }
5107
5108 for constraint_to_migrate in constraints_to_migrate.iter() {
5109 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::End
5110 && constraint_to_migrate.is_point_point
5111 {
5112 points_constrained_to_new_segment_end.insert(constraint_to_migrate.other_entity_id);
5113 }
5114 }
5115
5116 for constraint_to_migrate in constraints_to_migrate.iter() {
5117 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment
5119 && (points_constrained_to_new_segment_start.contains(&constraint_to_migrate.other_entity_id)
5120 || points_constrained_to_new_segment_end.contains(&constraint_to_migrate.other_entity_id))
5121 {
5122 continue; }
5124
5125 let constraint_segments = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment {
5126 vec![constraint_to_migrate.other_entity_id.into(), new_segment_id.into()]
5127 } else {
5128 let target_endpoint_id = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Start
5129 {
5130 new_segment_start_point_id
5131 } else {
5132 new_segment_end_point_id
5133 };
5134 vec![target_endpoint_id.into(), constraint_to_migrate.other_entity_id.into()]
5135 };
5136 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
5137 segments: constraint_segments,
5138 }));
5139 }
5140
5141 let mut distance_constraints_to_re_add: Vec<(
5143 crate::frontend::api::Number,
5144 Option<crate::frontend::sketch::Point2d<crate::frontend::api::Number>>,
5145 crate::frontend::sketch::ConstraintSource,
5146 )> = Vec::new();
5147 if let (Some(original_start_id), Some(original_end_id)) =
5148 (original_segment_start_point_id, original_segment_end_point_id)
5149 {
5150 for obj in &edit_scene_graph_delta.new_graph.objects {
5151 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5152 continue;
5153 };
5154
5155 let Constraint::Distance(distance) = constraint else {
5156 continue;
5157 };
5158
5159 let references_start = distance.contains_point(original_start_id);
5160 let references_end = distance.contains_point(original_end_id);
5161
5162 if references_start && references_end {
5163 distance_constraints_to_re_add.push((
5164 distance.distance,
5165 distance.label_position.clone(),
5166 distance.source.clone(),
5167 ));
5168 }
5169 }
5170 }
5171
5172 if let Some(original_start_id) = original_segment_start_point_id {
5174 for (distance_value, label_position, source) in distance_constraints_to_re_add {
5175 batch_constraints.push(Constraint::Distance(crate::frontend::sketch::Distance {
5176 points: vec![original_start_id.into(), new_segment_end_point_id.into()],
5177 distance: distance_value,
5178 label_position,
5179 source,
5180 }));
5181 }
5182 }
5183
5184 if let Some(new_center_id) = new_segment_center_point_id {
5186 for (constraint, original_center_id) in center_point_constraints_to_migrate {
5187 let center_rewrite_map = std::collections::HashMap::from([(original_center_id, new_center_id)]);
5188 if let Some(rewritten) = rewrite_constraint_with_map(&constraint, ¢er_rewrite_map)
5189 && matches!(rewritten, Constraint::Coincident(_) | Constraint::Distance(_))
5190 {
5191 batch_constraints.push(rewritten);
5192 }
5193 }
5194 }
5195
5196 let mut angle_rewrite_map = std::collections::HashMap::from([(*segment_id, new_segment_id)]);
5198 if let Some(original_end_id) = original_segment_end_point_id {
5199 angle_rewrite_map.insert(original_end_id, new_segment_end_point_id);
5200 }
5201 for obj in &edit_scene_graph_delta.new_graph.objects {
5202 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5203 continue;
5204 };
5205
5206 let should_migrate = match constraint {
5207 Constraint::Parallel(parallel) => parallel.lines.contains(segment_id),
5208 Constraint::Perpendicular(perpendicular) => perpendicular.lines.contains(segment_id),
5209 Constraint::Midpoint(midpoint) => {
5210 midpoint.segment == *segment_id
5211 || original_segment_end_point_id.is_some_and(|end_id| midpoint.point == end_id)
5212 }
5213 Constraint::Horizontal(Horizontal::Line { line }) => line == segment_id,
5214 Constraint::Horizontal(Horizontal::Points { points }) => original_segment_end_point_id
5215 .is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
5216 Constraint::Vertical(Vertical::Line { line }) => line == segment_id,
5217 Constraint::Vertical(Vertical::Points { points }) => original_segment_end_point_id
5218 .is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
5219 _ => false,
5220 };
5221
5222 if should_migrate
5223 && let Some(migrated_constraint) = rewrite_constraint_with_map(constraint, &angle_rewrite_map)
5224 && matches!(
5225 migrated_constraint,
5226 Constraint::Midpoint(_)
5227 | Constraint::Parallel(_)
5228 | Constraint::Perpendicular(_)
5229 | Constraint::Horizontal(_)
5230 | Constraint::Vertical(_)
5231 )
5232 {
5233 batch_constraints.push(migrated_constraint);
5234 }
5235 }
5236
5237 let constraint_object_ids: Vec<ObjectId> = constraints_to_delete.to_vec();
5239
5240 let batch_result = frontend
5241 .batch_split_segment_operations(
5242 ctx,
5243 version,
5244 sketch_id,
5245 Vec::new(), batch_constraints,
5247 constraint_object_ids,
5248 crate::frontend::sketch::NewSegmentInfo {
5249 segment_id: new_segment_id,
5250 start_point_id: new_segment_start_point_id,
5251 end_point_id: new_segment_end_point_id,
5252 center_point_id: new_segment_center_point_id,
5253 },
5254 )
5255 .await
5256 .map_err(|e| format!("Failed to batch split segment operations: {}", e.error.message()));
5257 if let Ok((_, ref batch_delta)) = batch_result {
5259 invalidates_ids = invalidates_ids || batch_delta.invalidates_ids;
5260 }
5261 batch_result
5262 }
5263 };
5264
5265 match operation_result {
5266 Ok((source_delta, scene_graph_delta)) => {
5267 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
5269 last_result = Some((source_delta, scene_graph_delta.clone()));
5270 }
5271 Err(e) => {
5272 crate::logln!("Error executing trim operation {}: {}", op_index, e);
5273 }
5275 }
5276
5277 op_index += consumed_ops;
5278 }
5279
5280 let (source_delta, mut scene_graph_delta) =
5281 last_result.ok_or_else(|| "No operations were executed successfully".to_string())?;
5282 scene_graph_delta.invalidates_ids = invalidates_ids;
5284 Ok((source_delta, scene_graph_delta))
5285}