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