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