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