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;
25const EPSILON_COINCIDENT_TERMINATION_SNAP: f64 = 5e-2;
26
27fn suffix_to_unit(suffix: NumericSuffix) -> UnitLength {
29 match suffix {
30 NumericSuffix::Mm => UnitLength::Millimeters,
31 NumericSuffix::Cm => UnitLength::Centimeters,
32 NumericSuffix::M => UnitLength::Meters,
33 NumericSuffix::Inch => UnitLength::Inches,
34 NumericSuffix::Ft => UnitLength::Feet,
35 NumericSuffix::Yd => UnitLength::Yards,
36 _ => UnitLength::Millimeters,
37 }
38}
39
40fn number_to_unit(n: &Number, target_unit: UnitLength) -> f64 {
42 adjust_length(suffix_to_unit(n.units), n.value, target_unit).0
43}
44
45fn unit_to_number(value: f64, source_unit: UnitLength, target_suffix: NumericSuffix) -> Number {
47 let (value, _) = adjust_length(source_unit, value, suffix_to_unit(target_suffix));
48 Number {
49 value,
50 units: target_suffix,
51 }
52}
53
54fn normalize_trim_points_to_unit(points: &[Coords2d], default_unit: UnitLength) -> Vec<Coords2d> {
56 points
57 .iter()
58 .map(|point| Coords2d {
59 x: adjust_length(UnitLength::Millimeters, point.x, default_unit).0,
60 y: adjust_length(UnitLength::Millimeters, point.y, default_unit).0,
61 })
62 .collect()
63}
64
65#[derive(Debug, Clone, Copy)]
67pub struct Coords2d {
68 pub x: f64,
69 pub y: f64,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum LineEndpoint {
75 Start,
76 End,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum ArcPoint {
82 Start,
83 End,
84 Center,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum CirclePoint {
90 Start,
91 Center,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum TrimDirection {
97 Left,
98 Right,
99}
100
101#[derive(Debug, Clone)]
109pub enum TrimItem {
110 Spawn {
111 trim_spawn_seg_id: ObjectId,
112 trim_spawn_coords: Coords2d,
113 next_index: usize,
114 },
115 None {
116 next_index: usize,
117 },
118}
119
120#[derive(Debug, Clone)]
127pub enum TrimTermination {
128 SegEndPoint {
129 trim_termination_coords: Coords2d,
130 },
131 Intersection {
132 trim_termination_coords: Coords2d,
133 intersecting_seg_id: ObjectId,
134 },
135 TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
136 trim_termination_coords: Coords2d,
137 intersecting_seg_id: ObjectId,
138 other_segment_point_id: ObjectId,
139 },
140}
141
142#[derive(Debug, Clone)]
144pub struct TrimTerminations {
145 pub left_side: TrimTermination,
146 pub right_side: TrimTermination,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum AttachToEndpoint {
152 Start,
153 End,
154 Segment,
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum EndpointChanged {
160 Start,
161 End,
162}
163
164#[derive(Debug, Clone)]
166pub struct CoincidentData {
167 pub intersecting_seg_id: ObjectId,
168 pub intersecting_endpoint_point_id: Option<ObjectId>,
169 pub existing_point_segment_constraint_id: Option<ObjectId>,
170}
171
172#[derive(Debug, Clone)]
174pub struct ConstraintToMigrate {
175 pub constraint_id: ObjectId,
176 pub other_entity_id: ObjectId,
177 pub is_point_point: bool,
180 pub attach_to_endpoint: AttachToEndpoint,
181}
182
183#[derive(Debug, Clone)]
185pub enum TrimPlan {
186 DeleteSegment {
187 segment_id: ObjectId,
188 },
189 TailCut {
190 segment_id: ObjectId,
191 endpoint_changed: EndpointChanged,
192 ctor: SegmentCtor,
193 segment_or_point_to_make_coincident_to: ObjectId,
194 intersecting_endpoint_point_id: Option<ObjectId>,
195 constraint_ids_to_delete: Vec<ObjectId>,
196 additional_edited_segment_ids: Vec<ObjectId>,
197 },
198 ReplaceCircleWithArc {
199 circle_id: ObjectId,
200 arc_start_coords: Coords2d,
201 arc_end_coords: Coords2d,
202 arc_start_termination: Box<TrimTermination>,
203 arc_end_termination: Box<TrimTermination>,
204 },
205 SplitSegment {
206 segment_id: ObjectId,
207 left_trim_coords: Coords2d,
208 right_trim_coords: Coords2d,
209 original_end_coords: Coords2d,
210 left_side: Box<TrimTermination>,
211 right_side: Box<TrimTermination>,
212 left_side_coincident_data: CoincidentData,
213 right_side_coincident_data: CoincidentData,
214 constraints_to_migrate: Vec<ConstraintToMigrate>,
215 constraints_to_delete: Vec<ObjectId>,
216 },
217}
218
219fn lower_trim_plan(plan: &TrimPlan) -> Vec<TrimOperation> {
220 match plan {
221 TrimPlan::DeleteSegment { segment_id } => vec![TrimOperation::SimpleTrim {
222 segment_to_trim_id: *segment_id,
223 }],
224 TrimPlan::TailCut {
225 segment_id,
226 endpoint_changed,
227 ctor,
228 segment_or_point_to_make_coincident_to,
229 intersecting_endpoint_point_id,
230 constraint_ids_to_delete,
231 additional_edited_segment_ids,
232 } => {
233 let mut ops = vec![
234 TrimOperation::EditSegment {
235 segment_id: *segment_id,
236 ctor: ctor.clone(),
237 endpoint_changed: *endpoint_changed,
238 additional_edited_segment_ids: additional_edited_segment_ids.clone(),
239 },
240 TrimOperation::AddCoincidentConstraint {
241 segment_id: *segment_id,
242 endpoint_changed: *endpoint_changed,
243 segment_or_point_to_make_coincident_to: *segment_or_point_to_make_coincident_to,
244 intersecting_endpoint_point_id: *intersecting_endpoint_point_id,
245 },
246 ];
247 if !constraint_ids_to_delete.is_empty() {
248 ops.push(TrimOperation::DeleteConstraints {
249 constraint_ids: constraint_ids_to_delete.clone(),
250 });
251 }
252 ops
253 }
254 TrimPlan::ReplaceCircleWithArc {
255 circle_id,
256 arc_start_coords,
257 arc_end_coords,
258 arc_start_termination,
259 arc_end_termination,
260 } => vec![TrimOperation::ReplaceCircleWithArc {
261 circle_id: *circle_id,
262 arc_start_coords: *arc_start_coords,
263 arc_end_coords: *arc_end_coords,
264 arc_start_termination: arc_start_termination.clone(),
265 arc_end_termination: arc_end_termination.clone(),
266 }],
267 TrimPlan::SplitSegment {
268 segment_id,
269 left_trim_coords,
270 right_trim_coords,
271 original_end_coords,
272 left_side,
273 right_side,
274 left_side_coincident_data,
275 right_side_coincident_data,
276 constraints_to_migrate,
277 constraints_to_delete,
278 } => vec![TrimOperation::SplitSegment {
279 segment_id: *segment_id,
280 left_trim_coords: *left_trim_coords,
281 right_trim_coords: *right_trim_coords,
282 original_end_coords: *original_end_coords,
283 left_side: left_side.clone(),
284 right_side: right_side.clone(),
285 left_side_coincident_data: left_side_coincident_data.clone(),
286 right_side_coincident_data: right_side_coincident_data.clone(),
287 constraints_to_migrate: constraints_to_migrate.clone(),
288 constraints_to_delete: constraints_to_delete.clone(),
289 }],
290 }
291}
292
293fn trim_plan_modifies_geometry(plan: &TrimPlan) -> bool {
294 matches!(
295 plan,
296 TrimPlan::DeleteSegment { .. }
297 | TrimPlan::TailCut { .. }
298 | TrimPlan::ReplaceCircleWithArc { .. }
299 | TrimPlan::SplitSegment { .. }
300 )
301}
302
303fn rewrite_object_id(id: ObjectId, rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>) -> ObjectId {
304 rewrite_map.get(&id).copied().unwrap_or(id)
305}
306
307fn rewrite_constraint_segment(
308 segment: crate::frontend::sketch::ConstraintSegment,
309 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
310) -> crate::frontend::sketch::ConstraintSegment {
311 match segment {
312 crate::frontend::sketch::ConstraintSegment::Segment(id) => {
313 crate::frontend::sketch::ConstraintSegment::Segment(rewrite_object_id(id, rewrite_map))
314 }
315 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
316 crate::frontend::sketch::ConstraintSegment::Origin(origin)
317 }
318 }
319}
320
321fn rewrite_constraint_segments(
322 segments: &[crate::frontend::sketch::ConstraintSegment],
323 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
324) -> Vec<crate::frontend::sketch::ConstraintSegment> {
325 segments
326 .iter()
327 .copied()
328 .map(|segment| rewrite_constraint_segment(segment, rewrite_map))
329 .collect()
330}
331
332fn constraint_segments_reference_any(
333 segments: &[crate::frontend::sketch::ConstraintSegment],
334 ids: &std::collections::HashSet<ObjectId>,
335) -> bool {
336 segments.iter().any(|segment| match segment {
337 crate::frontend::sketch::ConstraintSegment::Segment(id) => ids.contains(id),
338 crate::frontend::sketch::ConstraintSegment::Origin(_) => false,
339 })
340}
341
342fn rewrite_constraint_with_map(
343 constraint: &Constraint,
344 rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
345) -> Option<Constraint> {
346 match constraint {
350 Constraint::Coincident(coincident) => Some(Constraint::Coincident(crate::frontend::sketch::Coincident {
351 segments: rewrite_constraint_segments(&coincident.segments, rewrite_map),
352 })),
353 Constraint::Distance(distance) => Some(Constraint::Distance(crate::frontend::sketch::Distance {
354 points: rewrite_constraint_segments(&distance.points, rewrite_map),
355 distance: distance.distance,
356 label_position: distance.label_position.clone(),
357 source: distance.source.clone(),
358 })),
359 Constraint::HorizontalDistance(distance) => {
360 Some(Constraint::HorizontalDistance(crate::frontend::sketch::Distance {
361 points: rewrite_constraint_segments(&distance.points, rewrite_map),
362 distance: distance.distance,
363 label_position: distance.label_position.clone(),
364 source: distance.source.clone(),
365 }))
366 }
367 Constraint::VerticalDistance(distance) => {
368 Some(Constraint::VerticalDistance(crate::frontend::sketch::Distance {
369 points: rewrite_constraint_segments(&distance.points, rewrite_map),
370 distance: distance.distance,
371 label_position: distance.label_position.clone(),
372 source: distance.source.clone(),
373 }))
374 }
375 Constraint::Radius(radius) => Some(Constraint::Radius(crate::frontend::sketch::Radius {
376 arc: rewrite_object_id(radius.arc, rewrite_map),
377 radius: radius.radius,
378 label_position: radius.label_position.clone(),
379 source: radius.source.clone(),
380 })),
381 Constraint::Diameter(diameter) => Some(Constraint::Diameter(crate::frontend::sketch::Diameter {
382 arc: rewrite_object_id(diameter.arc, rewrite_map),
383 diameter: diameter.diameter,
384 label_position: diameter.label_position.clone(),
385 source: diameter.source.clone(),
386 })),
387 Constraint::EqualRadius(equal_radius) => Some(Constraint::EqualRadius(crate::frontend::sketch::EqualRadius {
388 input: equal_radius
389 .input
390 .iter()
391 .map(|id| rewrite_object_id(*id, rewrite_map))
392 .collect(),
393 })),
394 Constraint::Midpoint(midpoint) => Some(Constraint::Midpoint(crate::frontend::sketch::Midpoint {
395 point: rewrite_object_id(midpoint.point, rewrite_map),
396 segment: rewrite_object_id(midpoint.segment, rewrite_map),
397 })),
398 Constraint::Tangent(tangent) => Some(Constraint::Tangent(crate::frontend::sketch::Tangent {
399 input: tangent
400 .input
401 .iter()
402 .map(|id| rewrite_object_id(*id, rewrite_map))
403 .collect(),
404 })),
405 Constraint::Symmetric(symmetric) => Some(Constraint::Symmetric(crate::frontend::sketch::Symmetric {
406 input: symmetric
407 .input
408 .iter()
409 .map(|id| rewrite_object_id(*id, rewrite_map))
410 .collect(),
411 axis: rewrite_object_id(symmetric.axis, rewrite_map),
412 })),
413 Constraint::Parallel(parallel) => Some(Constraint::Parallel(crate::frontend::sketch::Parallel {
414 lines: parallel
415 .lines
416 .iter()
417 .map(|id| rewrite_object_id(*id, rewrite_map))
418 .collect(),
419 })),
420 Constraint::Perpendicular(perpendicular) => {
421 Some(Constraint::Perpendicular(crate::frontend::sketch::Perpendicular {
422 lines: perpendicular
423 .lines
424 .iter()
425 .map(|id| rewrite_object_id(*id, rewrite_map))
426 .collect(),
427 }))
428 }
429 Constraint::Horizontal(horizontal) => match horizontal {
430 crate::front::Horizontal::Line { line } => {
431 Some(Constraint::Horizontal(crate::frontend::sketch::Horizontal::Line {
432 line: rewrite_object_id(*line, rewrite_map),
433 }))
434 }
435 crate::front::Horizontal::Points { points } => Some(Constraint::Horizontal(Horizontal::Points {
436 points: points
437 .iter()
438 .map(|point| match point {
439 crate::frontend::sketch::ConstraintSegment::Segment(point) => {
440 crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
441 }
442 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
443 crate::frontend::sketch::ConstraintSegment::Origin(*origin)
444 }
445 })
446 .collect(),
447 })),
448 },
449 Constraint::Vertical(vertical) => match vertical {
450 crate::front::Vertical::Line { line } => {
451 Some(Constraint::Vertical(crate::frontend::sketch::Vertical::Line {
452 line: rewrite_object_id(*line, rewrite_map),
453 }))
454 }
455 crate::front::Vertical::Points { points } => Some(Constraint::Vertical(Vertical::Points {
456 points: points
457 .iter()
458 .map(|point| match point {
459 crate::frontend::sketch::ConstraintSegment::Segment(point) => {
460 crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
461 }
462 crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
463 crate::frontend::sketch::ConstraintSegment::Origin(*origin)
464 }
465 })
466 .collect(),
467 })),
468 },
469 Constraint::Angle(_) | Constraint::Fixed(_) | Constraint::LinesEqualLength(_) => None,
470 }
471}
472
473fn point_axis_constraint_references_point(constraint: &Constraint, point_id: ObjectId) -> bool {
474 match constraint {
477 Constraint::Horizontal(Horizontal::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
478 Constraint::Vertical(Vertical::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
479 Constraint::Angle(_)
480 | Constraint::Coincident(_)
481 | Constraint::Diameter(_)
482 | Constraint::Distance(_)
483 | Constraint::EqualRadius(_)
484 | Constraint::Fixed(_)
485 | Constraint::Horizontal(Horizontal::Line { .. })
486 | Constraint::HorizontalDistance(_)
487 | Constraint::LinesEqualLength(_)
488 | Constraint::Midpoint(_)
489 | Constraint::Parallel(_)
490 | Constraint::Perpendicular(_)
491 | Constraint::Radius(_)
492 | Constraint::Symmetric(_)
493 | Constraint::Tangent(_)
494 | Constraint::Vertical(Vertical::Line { .. })
495 | Constraint::VerticalDistance(_) => false,
496 }
497}
498
499fn owner_or_segment_id(objects: &[Object], segment_id: ObjectId) -> ObjectId {
500 if let Some(segment_object) = objects.iter().find(|obj| obj.id == segment_id)
501 && let ObjectKind::Segment {
502 segment: Segment::Point(point),
503 } = &segment_object.kind
504 && let Some(owner_id) = point.owner
505 {
506 owner_id
507 } else {
508 segment_id
509 }
510}
511
512fn segment_id_is_or_is_owned_by_curve(objects: &[Object], segment_id: ObjectId) -> bool {
513 objects.iter().find(|obj| obj.id == segment_id).is_some_and(|object| {
514 let ObjectKind::Segment { segment } = &object.kind else {
515 return false;
516 };
517
518 match segment {
519 Segment::Arc(_) | Segment::Circle(_) => true,
520 Segment::Point(point) => point.owner.is_some_and(|owner_id| {
521 objects.iter().find(|obj| obj.id == owner_id).is_some_and(|owner| {
522 matches!(
523 owner.kind,
524 ObjectKind::Segment {
525 segment: Segment::Arc(_) | Segment::Circle(_)
526 }
527 )
528 })
529 }),
530 _ => false,
531 }
532 })
533}
534
535fn sketch_segment_ids_for_segment(objects: &[Object], segment_id: ObjectId) -> Vec<ObjectId> {
536 objects
537 .iter()
538 .find_map(|obj| {
539 let ObjectKind::Sketch(sketch) = &obj.kind else {
540 return None;
541 };
542
543 sketch.segments.contains(&segment_id).then(|| sketch.segments.clone())
544 })
545 .unwrap_or_default()
546}
547
548#[derive(Debug, Clone)]
549#[allow(clippy::large_enum_variant)]
550pub enum TrimOperation {
551 SimpleTrim {
552 segment_to_trim_id: ObjectId,
553 },
554 EditSegment {
555 segment_id: ObjectId,
556 ctor: SegmentCtor,
557 endpoint_changed: EndpointChanged,
558 additional_edited_segment_ids: Vec<ObjectId>,
559 },
560 AddCoincidentConstraint {
561 segment_id: ObjectId,
562 endpoint_changed: EndpointChanged,
563 segment_or_point_to_make_coincident_to: ObjectId,
564 intersecting_endpoint_point_id: Option<ObjectId>,
565 },
566 SplitSegment {
567 segment_id: ObjectId,
568 left_trim_coords: Coords2d,
569 right_trim_coords: Coords2d,
570 original_end_coords: Coords2d,
571 left_side: Box<TrimTermination>,
572 right_side: Box<TrimTermination>,
573 left_side_coincident_data: CoincidentData,
574 right_side_coincident_data: CoincidentData,
575 constraints_to_migrate: Vec<ConstraintToMigrate>,
576 constraints_to_delete: Vec<ObjectId>,
577 },
578 ReplaceCircleWithArc {
579 circle_id: ObjectId,
580 arc_start_coords: Coords2d,
581 arc_end_coords: Coords2d,
582 arc_start_termination: Box<TrimTermination>,
583 arc_end_termination: Box<TrimTermination>,
584 },
585 DeleteConstraints {
586 constraint_ids: Vec<ObjectId>,
587 },
588}
589
590pub fn is_point_on_line_segment(
594 point: Coords2d,
595 segment_start: Coords2d,
596 segment_end: Coords2d,
597 epsilon: f64,
598) -> Option<Coords2d> {
599 let dx = segment_end.x - segment_start.x;
600 let dy = segment_end.y - segment_start.y;
601 let segment_length_sq = dx * dx + dy * dy;
602
603 if segment_length_sq < EPSILON_PARALLEL {
604 let dist_sq = (point.x - segment_start.x) * (point.x - segment_start.x)
606 + (point.y - segment_start.y) * (point.y - segment_start.y);
607 if dist_sq <= epsilon * epsilon {
608 return Some(point);
609 }
610 return None;
611 }
612
613 let point_dx = point.x - segment_start.x;
614 let point_dy = point.y - segment_start.y;
615 let projection_param = (point_dx * dx + point_dy * dy) / segment_length_sq;
616
617 if !(0.0..=1.0).contains(&projection_param) {
619 return None;
620 }
621
622 let projected_point = Coords2d {
624 x: segment_start.x + projection_param * dx,
625 y: segment_start.y + projection_param * dy,
626 };
627
628 let dist_dx = point.x - projected_point.x;
630 let dist_dy = point.y - projected_point.y;
631 let distance_sq = dist_dx * dist_dx + dist_dy * dist_dy;
632
633 if distance_sq <= epsilon * epsilon {
634 Some(point)
635 } else {
636 None
637 }
638}
639
640pub fn line_segment_intersection(
644 line1_start: Coords2d,
645 line1_end: Coords2d,
646 line2_start: Coords2d,
647 line2_end: Coords2d,
648 epsilon: f64,
649) -> Option<Coords2d> {
650 if let Some(point) = is_point_on_line_segment(line1_start, line2_start, line2_end, epsilon) {
652 return Some(point);
653 }
654
655 if let Some(point) = is_point_on_line_segment(line1_end, line2_start, line2_end, epsilon) {
656 return Some(point);
657 }
658
659 if let Some(point) = is_point_on_line_segment(line2_start, line1_start, line1_end, epsilon) {
660 return Some(point);
661 }
662
663 if let Some(point) = is_point_on_line_segment(line2_end, line1_start, line1_end, epsilon) {
664 return Some(point);
665 }
666
667 let x1 = line1_start.x;
669 let y1 = line1_start.y;
670 let x2 = line1_end.x;
671 let y2 = line1_end.y;
672 let x3 = line2_start.x;
673 let y3 = line2_start.y;
674 let x4 = line2_end.x;
675 let y4 = line2_end.y;
676
677 let denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
678 if denominator.abs() < EPSILON_PARALLEL {
679 return None;
681 }
682
683 let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denominator;
684 let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denominator;
685
686 if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
688 let x = x1 + t * (x2 - x1);
689 let y = y1 + t * (y2 - y1);
690 return Some(Coords2d { x, y });
691 }
692
693 None
694}
695
696pub fn project_point_onto_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
701 let dx = segment_end.x - segment_start.x;
702 let dy = segment_end.y - segment_start.y;
703 let segment_length_sq = dx * dx + dy * dy;
704
705 if segment_length_sq < EPSILON_PARALLEL {
706 return 0.0;
708 }
709
710 let point_dx = point.x - segment_start.x;
711 let point_dy = point.y - segment_start.y;
712
713 (point_dx * dx + point_dy * dy) / segment_length_sq
714}
715
716pub fn perpendicular_distance_to_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
720 let dx = segment_end.x - segment_start.x;
721 let dy = segment_end.y - segment_start.y;
722 let segment_length_sq = dx * dx + dy * dy;
723
724 if segment_length_sq < EPSILON_PARALLEL {
725 let dist_dx = point.x - segment_start.x;
727 let dist_dy = point.y - segment_start.y;
728 return (dist_dx * dist_dx + dist_dy * dist_dy).sqrt();
729 }
730
731 let point_dx = point.x - segment_start.x;
733 let point_dy = point.y - segment_start.y;
734
735 let t = (point_dx * dx + point_dy * dy) / segment_length_sq;
737
738 let clamped_t = t.clamp(0.0, 1.0);
740 let closest_point = Coords2d {
741 x: segment_start.x + clamped_t * dx,
742 y: segment_start.y + clamped_t * dy,
743 };
744
745 let dist_dx = point.x - closest_point.x;
747 let dist_dy = point.y - closest_point.y;
748 (dist_dx * dist_dx + dist_dy * dist_dy).sqrt()
749}
750
751pub fn is_point_on_arc(point: Coords2d, center: Coords2d, start: Coords2d, end: Coords2d, epsilon: f64) -> bool {
755 let radius = ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
757
758 let dist_from_center =
760 ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
761 if (dist_from_center - radius).abs() > epsilon {
762 return false;
763 }
764
765 let start_angle = libm::atan2(start.y - center.y, start.x - center.x);
767 let end_angle = libm::atan2(end.y - center.y, end.x - center.x);
768 let point_angle = libm::atan2(point.y - center.y, point.x - center.x);
769
770 let normalize_angle = |angle: f64| -> f64 {
772 if !angle.is_finite() {
773 return angle;
774 }
775 let mut normalized = angle;
776 while normalized < 0.0 {
777 normalized += TAU;
778 }
779 while normalized >= TAU {
780 normalized -= TAU;
781 }
782 normalized
783 };
784
785 let normalized_start = normalize_angle(start_angle);
786 let normalized_end = normalize_angle(end_angle);
787 let normalized_point = normalize_angle(point_angle);
788
789 if normalized_start < normalized_end {
793 normalized_point >= normalized_start && normalized_point <= normalized_end
795 } else {
796 normalized_point >= normalized_start || normalized_point <= normalized_end
798 }
799}
800
801pub fn line_arc_intersections(
805 line_start: Coords2d,
806 line_end: Coords2d,
807 arc_center: Coords2d,
808 arc_start: Coords2d,
809 arc_end: Coords2d,
810 epsilon: f64,
811) -> Vec<(f64, Coords2d)> {
812 let radius = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
814 + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
815 .sqrt();
816
817 let translated_line_start = Coords2d {
819 x: line_start.x - arc_center.x,
820 y: line_start.y - arc_center.y,
821 };
822 let translated_line_end = Coords2d {
823 x: line_end.x - arc_center.x,
824 y: line_end.y - arc_center.y,
825 };
826
827 let dx = translated_line_end.x - translated_line_start.x;
829 let dy = translated_line_end.y - translated_line_start.y;
830
831 let a = dx * dx + dy * dy;
838 let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
839 let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
840 - radius * radius;
841
842 let discriminant = b * b - 4.0 * a * c;
843
844 if discriminant < 0.0 {
845 return Vec::new();
847 }
848
849 if a.abs() < EPSILON_PARALLEL {
850 let dist_from_center = (translated_line_start.x * translated_line_start.x
852 + translated_line_start.y * translated_line_start.y)
853 .sqrt();
854 if (dist_from_center - radius).abs() <= epsilon {
855 let point = line_start;
857 if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
858 return vec![(0.0, point)];
859 }
860 }
861 return Vec::new();
862 }
863
864 let sqrt_discriminant = discriminant.sqrt();
865 let t1 = (-b - sqrt_discriminant) / (2.0 * a);
866 let t2 = (-b + sqrt_discriminant) / (2.0 * a);
867
868 let mut candidates: Vec<(f64, Coords2d)> = Vec::new();
870 if (0.0..=1.0).contains(&t1) {
871 let point = Coords2d {
872 x: line_start.x + t1 * (line_end.x - line_start.x),
873 y: line_start.y + t1 * (line_end.y - line_start.y),
874 };
875 candidates.push((t1, point));
876 }
877 if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
878 let point = Coords2d {
879 x: line_start.x + t2 * (line_end.x - line_start.x),
880 y: line_start.y + t2 * (line_end.y - line_start.y),
881 };
882 candidates.push((t2, point));
883 }
884
885 candidates.retain(|(_, point)| is_point_on_arc(*point, arc_center, arc_start, arc_end, epsilon));
886 candidates.sort_by(|(a_t, _), (b_t, _)| a_t.partial_cmp(b_t).unwrap_or(std::cmp::Ordering::Equal));
887 candidates
888}
889
890pub fn line_arc_intersection(
894 line_start: Coords2d,
895 line_end: Coords2d,
896 arc_center: Coords2d,
897 arc_start: Coords2d,
898 arc_end: Coords2d,
899 epsilon: f64,
900) -> Option<Coords2d> {
901 line_arc_intersections(line_start, line_end, arc_center, arc_start, arc_end, epsilon)
902 .into_iter()
903 .map(|(_, point)| point)
904 .next()
905}
906
907pub fn line_circle_intersections(
912 line_start: Coords2d,
913 line_end: Coords2d,
914 circle_center: Coords2d,
915 radius: f64,
916 epsilon: f64,
917) -> Vec<(f64, Coords2d)> {
918 let translated_line_start = Coords2d {
920 x: line_start.x - circle_center.x,
921 y: line_start.y - circle_center.y,
922 };
923 let translated_line_end = Coords2d {
924 x: line_end.x - circle_center.x,
925 y: line_end.y - circle_center.y,
926 };
927
928 let dx = translated_line_end.x - translated_line_start.x;
929 let dy = translated_line_end.y - translated_line_start.y;
930 let a = dx * dx + dy * dy;
931 let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
932 let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
933 - radius * radius;
934
935 if a.abs() < EPSILON_PARALLEL {
936 return Vec::new();
937 }
938
939 let discriminant = b * b - 4.0 * a * c;
940 if discriminant < 0.0 {
941 return Vec::new();
942 }
943
944 let sqrt_discriminant = discriminant.sqrt();
945 let mut intersections = Vec::new();
946
947 let t1 = (-b - sqrt_discriminant) / (2.0 * a);
948 if (0.0..=1.0).contains(&t1) {
949 intersections.push((
950 t1,
951 Coords2d {
952 x: line_start.x + t1 * (line_end.x - line_start.x),
953 y: line_start.y + t1 * (line_end.y - line_start.y),
954 },
955 ));
956 }
957
958 let t2 = (-b + sqrt_discriminant) / (2.0 * a);
959 if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
960 intersections.push((
961 t2,
962 Coords2d {
963 x: line_start.x + t2 * (line_end.x - line_start.x),
964 y: line_start.y + t2 * (line_end.y - line_start.y),
965 },
966 ));
967 }
968
969 intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
970 intersections
971}
972
973pub fn project_point_onto_circle(point: Coords2d, center: Coords2d, start: Coords2d) -> f64 {
979 let normalize_angle = |angle: f64| -> f64 {
980 if !angle.is_finite() {
981 return angle;
982 }
983 let mut normalized = angle;
984 while normalized < 0.0 {
985 normalized += TAU;
986 }
987 while normalized >= TAU {
988 normalized -= TAU;
989 }
990 normalized
991 };
992
993 let start_angle = normalize_angle(libm::atan2(start.y - center.y, start.x - center.x));
994 let point_angle = normalize_angle(libm::atan2(point.y - center.y, point.x - center.x));
995 let delta_ccw = (point_angle - start_angle).rem_euclid(TAU);
996 delta_ccw / TAU
997}
998
999fn is_point_on_circle(point: Coords2d, center: Coords2d, radius: f64, epsilon: f64) -> bool {
1000 let dist = ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
1001 (dist - radius).abs() <= epsilon
1002}
1003
1004pub fn project_point_onto_arc(point: Coords2d, arc_center: Coords2d, arc_start: Coords2d, arc_end: Coords2d) -> f64 {
1007 let start_angle = libm::atan2(arc_start.y - arc_center.y, arc_start.x - arc_center.x);
1009 let end_angle = libm::atan2(arc_end.y - arc_center.y, arc_end.x - arc_center.x);
1010 let point_angle = libm::atan2(point.y - arc_center.y, point.x - arc_center.x);
1011
1012 let normalize_angle = |angle: f64| -> f64 {
1014 if !angle.is_finite() {
1015 return angle;
1016 }
1017 let mut normalized = angle;
1018 while normalized < 0.0 {
1019 normalized += TAU;
1020 }
1021 while normalized >= TAU {
1022 normalized -= TAU;
1023 }
1024 normalized
1025 };
1026
1027 let normalized_start = normalize_angle(start_angle);
1028 let normalized_end = normalize_angle(end_angle);
1029 let normalized_point = normalize_angle(point_angle);
1030
1031 let arc_length = if normalized_start < normalized_end {
1033 normalized_end - normalized_start
1034 } else {
1035 TAU - normalized_start + normalized_end
1037 };
1038
1039 if arc_length < EPSILON_PARALLEL {
1040 return 0.0;
1042 }
1043
1044 let point_arc_length = if normalized_start < normalized_end {
1046 if normalized_point >= normalized_start && normalized_point <= normalized_end {
1047 normalized_point - normalized_start
1048 } else {
1049 let dist_to_start = libm::fmin(
1051 (normalized_point - normalized_start).abs(),
1052 TAU - (normalized_point - normalized_start).abs(),
1053 );
1054 let dist_to_end = libm::fmin(
1055 (normalized_point - normalized_end).abs(),
1056 TAU - (normalized_point - normalized_end).abs(),
1057 );
1058 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
1059 }
1060 } else {
1061 if normalized_point >= normalized_start || normalized_point <= normalized_end {
1063 if normalized_point >= normalized_start {
1064 normalized_point - normalized_start
1065 } else {
1066 TAU - normalized_start + normalized_point
1067 }
1068 } else {
1069 let dist_to_start = libm::fmin(
1071 (normalized_point - normalized_start).abs(),
1072 TAU - (normalized_point - normalized_start).abs(),
1073 );
1074 let dist_to_end = libm::fmin(
1075 (normalized_point - normalized_end).abs(),
1076 TAU - (normalized_point - normalized_end).abs(),
1077 );
1078 return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
1079 }
1080 };
1081
1082 point_arc_length / arc_length
1084}
1085
1086pub fn arc_arc_intersections(
1090 arc1_center: Coords2d,
1091 arc1_start: Coords2d,
1092 arc1_end: Coords2d,
1093 arc2_center: Coords2d,
1094 arc2_start: Coords2d,
1095 arc2_end: Coords2d,
1096 epsilon: f64,
1097) -> Vec<Coords2d> {
1098 let r1 = ((arc1_start.x - arc1_center.x) * (arc1_start.x - arc1_center.x)
1100 + (arc1_start.y - arc1_center.y) * (arc1_start.y - arc1_center.y))
1101 .sqrt();
1102 let r2 = ((arc2_start.x - arc2_center.x) * (arc2_start.x - arc2_center.x)
1103 + (arc2_start.y - arc2_center.y) * (arc2_start.y - arc2_center.y))
1104 .sqrt();
1105
1106 let dx = arc2_center.x - arc1_center.x;
1108 let dy = arc2_center.y - arc1_center.y;
1109 let d = (dx * dx + dy * dy).sqrt();
1110
1111 if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon {
1113 return Vec::new();
1115 }
1116
1117 if d < EPSILON_PARALLEL {
1119 return Vec::new();
1121 }
1122
1123 let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1126 let h_sq = r1 * r1 - a * a;
1127
1128 if h_sq < 0.0 {
1130 return Vec::new();
1131 }
1132
1133 let h = h_sq.sqrt();
1134
1135 if h.is_nan() {
1137 return Vec::new();
1138 }
1139
1140 let ux = dx / d;
1142 let uy = dy / d;
1143
1144 let px = -uy;
1146 let py = ux;
1147
1148 let mid_point = Coords2d {
1150 x: arc1_center.x + a * ux,
1151 y: arc1_center.y + a * uy,
1152 };
1153
1154 let intersection1 = Coords2d {
1156 x: mid_point.x + h * px,
1157 y: mid_point.y + h * py,
1158 };
1159 let intersection2 = Coords2d {
1160 x: mid_point.x - h * px,
1161 y: mid_point.y - h * py,
1162 };
1163
1164 let mut candidates: Vec<Coords2d> = Vec::new();
1166
1167 if is_point_on_arc(intersection1, arc1_center, arc1_start, arc1_end, epsilon)
1168 && is_point_on_arc(intersection1, arc2_center, arc2_start, arc2_end, epsilon)
1169 {
1170 candidates.push(intersection1);
1171 }
1172
1173 if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1174 if is_point_on_arc(intersection2, arc1_center, arc1_start, arc1_end, epsilon)
1176 && is_point_on_arc(intersection2, arc2_center, arc2_start, arc2_end, epsilon)
1177 {
1178 candidates.push(intersection2);
1179 }
1180 }
1181
1182 candidates
1183}
1184
1185pub fn arc_arc_intersection(
1190 arc1_center: Coords2d,
1191 arc1_start: Coords2d,
1192 arc1_end: Coords2d,
1193 arc2_center: Coords2d,
1194 arc2_start: Coords2d,
1195 arc2_end: Coords2d,
1196 epsilon: f64,
1197) -> Option<Coords2d> {
1198 arc_arc_intersections(
1199 arc1_center,
1200 arc1_start,
1201 arc1_end,
1202 arc2_center,
1203 arc2_start,
1204 arc2_end,
1205 epsilon,
1206 )
1207 .first()
1208 .copied()
1209}
1210
1211pub fn circle_arc_intersections(
1215 circle_center: Coords2d,
1216 circle_radius: f64,
1217 arc_center: Coords2d,
1218 arc_start: Coords2d,
1219 arc_end: Coords2d,
1220 epsilon: f64,
1221) -> Vec<Coords2d> {
1222 let r1 = circle_radius;
1223 let r2 = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
1224 + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
1225 .sqrt();
1226
1227 let dx = arc_center.x - circle_center.x;
1228 let dy = arc_center.y - circle_center.y;
1229 let d = (dx * dx + dy * dy).sqrt();
1230
1231 if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon || d < EPSILON_PARALLEL {
1232 return Vec::new();
1233 }
1234
1235 let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1236 let h_sq = r1 * r1 - a * a;
1237 if h_sq < 0.0 {
1238 return Vec::new();
1239 }
1240 let h = h_sq.sqrt();
1241 if h.is_nan() {
1242 return Vec::new();
1243 }
1244
1245 let ux = dx / d;
1246 let uy = dy / d;
1247 let px = -uy;
1248 let py = ux;
1249 let mid_point = Coords2d {
1250 x: circle_center.x + a * ux,
1251 y: circle_center.y + a * uy,
1252 };
1253
1254 let intersection1 = Coords2d {
1255 x: mid_point.x + h * px,
1256 y: mid_point.y + h * py,
1257 };
1258 let intersection2 = Coords2d {
1259 x: mid_point.x - h * px,
1260 y: mid_point.y - h * py,
1261 };
1262
1263 let mut intersections = Vec::new();
1264 if is_point_on_arc(intersection1, arc_center, arc_start, arc_end, epsilon) {
1265 intersections.push(intersection1);
1266 }
1267 if ((intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon)
1268 && is_point_on_arc(intersection2, arc_center, arc_start, arc_end, epsilon)
1269 {
1270 intersections.push(intersection2);
1271 }
1272 intersections
1273}
1274
1275pub fn circle_circle_intersections(
1279 circle1_center: Coords2d,
1280 circle1_radius: f64,
1281 circle2_center: Coords2d,
1282 circle2_radius: f64,
1283 epsilon: f64,
1284) -> Vec<Coords2d> {
1285 let dx = circle2_center.x - circle1_center.x;
1286 let dy = circle2_center.y - circle1_center.y;
1287 let d = (dx * dx + dy * dy).sqrt();
1288
1289 if d > circle1_radius + circle2_radius + epsilon
1290 || d < (circle1_radius - circle2_radius).abs() - epsilon
1291 || d < EPSILON_PARALLEL
1292 {
1293 return Vec::new();
1294 }
1295
1296 let a = (circle1_radius * circle1_radius - circle2_radius * circle2_radius + d * d) / (2.0 * d);
1297 let h_sq = circle1_radius * circle1_radius - a * a;
1298 if h_sq < 0.0 {
1299 return Vec::new();
1300 }
1301
1302 let h = if h_sq <= epsilon { 0.0 } else { h_sq.sqrt() };
1303 if h.is_nan() {
1304 return Vec::new();
1305 }
1306
1307 let ux = dx / d;
1308 let uy = dy / d;
1309 let px = -uy;
1310 let py = ux;
1311
1312 let mid_point = Coords2d {
1313 x: circle1_center.x + a * ux,
1314 y: circle1_center.y + a * uy,
1315 };
1316
1317 let intersection1 = Coords2d {
1318 x: mid_point.x + h * px,
1319 y: mid_point.y + h * py,
1320 };
1321 let intersection2 = Coords2d {
1322 x: mid_point.x - h * px,
1323 y: mid_point.y - h * py,
1324 };
1325
1326 let mut intersections = vec![intersection1];
1327 if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1328 intersections.push(intersection2);
1329 }
1330 intersections
1331}
1332
1333fn get_point_coords_from_native(objects: &[Object], point_id: ObjectId, default_unit: UnitLength) -> Option<Coords2d> {
1336 let point_obj = objects.get(point_id.0)?;
1337
1338 let ObjectKind::Segment { segment } = &point_obj.kind else {
1340 return None;
1341 };
1342
1343 let Segment::Point(point) = segment else {
1344 return None;
1345 };
1346
1347 Some(Coords2d {
1349 x: number_to_unit(&point.position.x, default_unit),
1350 y: number_to_unit(&point.position.y, default_unit),
1351 })
1352}
1353
1354pub fn get_position_coords_for_line(
1357 segment_obj: &Object,
1358 which: LineEndpoint,
1359 objects: &[Object],
1360 default_unit: UnitLength,
1361) -> Option<Coords2d> {
1362 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1363 return None;
1364 };
1365
1366 let Segment::Line(line) = segment else {
1367 return None;
1368 };
1369
1370 let point_id = match which {
1372 LineEndpoint::Start => line.start,
1373 LineEndpoint::End => line.end,
1374 };
1375
1376 get_point_coords_from_native(objects, point_id, default_unit)
1377}
1378
1379fn is_point_coincident_with_segment_native(point_id: ObjectId, segment_id: ObjectId, objects: &[Object]) -> bool {
1381 for obj in objects {
1383 let ObjectKind::Constraint { constraint } = &obj.kind else {
1384 continue;
1385 };
1386
1387 let Constraint::Coincident(coincident) = constraint else {
1388 continue;
1389 };
1390
1391 let has_point = coincident.contains_segment(point_id);
1393 let has_segment = coincident.contains_segment(segment_id);
1394
1395 if has_point && has_segment {
1396 return true;
1397 }
1398 }
1399 false
1400}
1401
1402pub fn get_position_coords_from_arc(
1404 segment_obj: &Object,
1405 which: ArcPoint,
1406 objects: &[Object],
1407 default_unit: UnitLength,
1408) -> Option<Coords2d> {
1409 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1410 return None;
1411 };
1412
1413 let Segment::Arc(arc) = segment else {
1414 return None;
1415 };
1416
1417 let point_id = match which {
1419 ArcPoint::Start => arc.start,
1420 ArcPoint::End => arc.end,
1421 ArcPoint::Center => arc.center,
1422 };
1423
1424 get_point_coords_from_native(objects, point_id, default_unit)
1425}
1426
1427pub fn get_position_coords_from_circle(
1429 segment_obj: &Object,
1430 which: CirclePoint,
1431 objects: &[Object],
1432 default_unit: UnitLength,
1433) -> Option<Coords2d> {
1434 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1435 return None;
1436 };
1437
1438 let Segment::Circle(circle) = segment else {
1439 return None;
1440 };
1441
1442 let point_id = match which {
1443 CirclePoint::Start => circle.start,
1444 CirclePoint::Center => circle.center,
1445 };
1446
1447 get_point_coords_from_native(objects, point_id, default_unit)
1448}
1449
1450#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1452enum CurveKind {
1453 Line,
1454 Circular,
1455}
1456
1457#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1459enum CurveDomain {
1460 Open,
1461 Closed,
1462}
1463
1464#[derive(Debug, Clone, Copy)]
1466struct CurveHandle {
1467 segment_id: ObjectId,
1468 kind: CurveKind,
1469 domain: CurveDomain,
1470 start: Coords2d,
1471 end: Coords2d,
1472 center: Option<Coords2d>,
1473 radius: Option<f64>,
1474}
1475
1476impl CurveHandle {
1477 fn project_for_trim(self, point: Coords2d) -> Result<f64, String> {
1478 match (self.kind, self.domain) {
1479 (CurveKind::Line, CurveDomain::Open) => Ok(project_point_onto_segment(point, self.start, self.end)),
1480 (CurveKind::Circular, CurveDomain::Open) => {
1481 let center = self
1482 .center
1483 .ok_or_else(|| format!("Curve {} missing center for arc projection", self.segment_id.0))?;
1484 Ok(project_point_onto_arc(point, center, self.start, self.end))
1485 }
1486 (CurveKind::Circular, CurveDomain::Closed) => {
1487 let center = self
1488 .center
1489 .ok_or_else(|| format!("Curve {} missing center for circle projection", self.segment_id.0))?;
1490 Ok(project_point_onto_circle(point, center, self.start))
1491 }
1492 (CurveKind::Line, CurveDomain::Closed) => Err(format!(
1493 "Invalid curve state: line {} cannot be closed",
1494 self.segment_id.0
1495 )),
1496 }
1497 }
1498}
1499
1500fn load_curve_handle(
1502 segment_obj: &Object,
1503 objects: &[Object],
1504 default_unit: UnitLength,
1505) -> Result<CurveHandle, String> {
1506 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1507 return Err("Object is not a segment".to_owned());
1508 };
1509
1510 match segment {
1511 Segment::Line(_) => {
1512 let start = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit)
1513 .ok_or_else(|| format!("Could not get line start for segment {}", segment_obj.id.0))?;
1514 let end = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit)
1515 .ok_or_else(|| format!("Could not get line end for segment {}", segment_obj.id.0))?;
1516 Ok(CurveHandle {
1517 segment_id: segment_obj.id,
1518 kind: CurveKind::Line,
1519 domain: CurveDomain::Open,
1520 start,
1521 end,
1522 center: None,
1523 radius: None,
1524 })
1525 }
1526 Segment::Arc(_) => {
1527 let start = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit)
1528 .ok_or_else(|| format!("Could not get arc start for segment {}", segment_obj.id.0))?;
1529 let end = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit)
1530 .ok_or_else(|| format!("Could not get arc end for segment {}", segment_obj.id.0))?;
1531 let center = get_position_coords_from_arc(segment_obj, ArcPoint::Center, objects, default_unit)
1532 .ok_or_else(|| format!("Could not get arc center for segment {}", segment_obj.id.0))?;
1533 let radius =
1534 ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1535 Ok(CurveHandle {
1536 segment_id: segment_obj.id,
1537 kind: CurveKind::Circular,
1538 domain: CurveDomain::Open,
1539 start,
1540 end,
1541 center: Some(center),
1542 radius: Some(radius),
1543 })
1544 }
1545 Segment::Circle(_) => {
1546 let start = get_position_coords_from_circle(segment_obj, CirclePoint::Start, objects, default_unit)
1547 .ok_or_else(|| format!("Could not get circle start for segment {}", segment_obj.id.0))?;
1548 let center = get_position_coords_from_circle(segment_obj, CirclePoint::Center, objects, default_unit)
1549 .ok_or_else(|| format!("Could not get circle center for segment {}", segment_obj.id.0))?;
1550 let radius =
1551 ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1552 Ok(CurveHandle {
1553 segment_id: segment_obj.id,
1554 kind: CurveKind::Circular,
1555 domain: CurveDomain::Closed,
1556 start,
1557 end: start,
1559 center: Some(center),
1560 radius: Some(radius),
1561 })
1562 }
1563 Segment::Point(_) => Err(format!(
1564 "Point segment {} cannot be used as trim curve",
1565 segment_obj.id.0
1566 )),
1567 }
1568}
1569
1570fn project_point_onto_curve(curve: CurveHandle, point: Coords2d) -> Result<f64, String> {
1571 curve.project_for_trim(point)
1572}
1573
1574fn curve_contains_point(curve: CurveHandle, point: Coords2d, epsilon: f64) -> bool {
1575 match (curve.kind, curve.domain) {
1576 (CurveKind::Line, CurveDomain::Open) => {
1577 let t = project_point_onto_segment(point, curve.start, curve.end);
1578 (0.0..=1.0).contains(&t) && perpendicular_distance_to_segment(point, curve.start, curve.end) <= epsilon
1579 }
1580 (CurveKind::Circular, CurveDomain::Open) => curve
1581 .center
1582 .is_some_and(|center| is_point_on_arc(point, center, curve.start, curve.end, epsilon)),
1583 (CurveKind::Circular, CurveDomain::Closed) => curve.center.is_some_and(|center| {
1584 let radius = curve
1585 .radius
1586 .unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
1587 is_point_on_circle(point, center, radius, epsilon)
1588 }),
1589 (CurveKind::Line, CurveDomain::Closed) => false,
1590 }
1591}
1592
1593fn curve_line_segment_intersections(
1594 curve: CurveHandle,
1595 line_start: Coords2d,
1596 line_end: Coords2d,
1597 epsilon: f64,
1598) -> Vec<(f64, Coords2d)> {
1599 match (curve.kind, curve.domain) {
1600 (CurveKind::Line, CurveDomain::Open) => {
1601 line_segment_intersection(line_start, line_end, curve.start, curve.end, epsilon)
1602 .map(|intersection| {
1603 (
1604 project_point_onto_segment(intersection, line_start, line_end),
1605 intersection,
1606 )
1607 })
1608 .into_iter()
1609 .collect()
1610 }
1611 (CurveKind::Circular, CurveDomain::Open) => curve
1612 .center
1613 .map(|center| line_arc_intersections(line_start, line_end, center, curve.start, curve.end, epsilon))
1614 .unwrap_or_default(),
1615 (CurveKind::Circular, CurveDomain::Closed) => {
1616 let Some(center) = curve.center else {
1617 return Vec::new();
1618 };
1619 let radius = curve
1620 .radius
1621 .unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
1622 line_circle_intersections(line_start, line_end, center, radius, epsilon)
1623 }
1624 (CurveKind::Line, CurveDomain::Closed) => Vec::new(),
1625 }
1626}
1627
1628fn curve_polyline_intersections(curve: CurveHandle, polyline: &[Coords2d], epsilon: f64) -> Vec<(Coords2d, usize)> {
1629 let mut intersections = Vec::new();
1630
1631 for i in 0..polyline.len().saturating_sub(1) {
1632 let p1 = polyline[i];
1633 let p2 = polyline[i + 1];
1634 for (_, intersection) in curve_line_segment_intersections(curve, p1, p2, epsilon) {
1635 intersections.push((intersection, i));
1636 }
1637 }
1638
1639 intersections
1640}
1641
1642fn curve_curve_intersections(curve: CurveHandle, other: CurveHandle, epsilon: f64) -> Vec<Coords2d> {
1643 match (curve.kind, curve.domain, other.kind, other.domain) {
1644 (CurveKind::Line, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => {
1645 line_segment_intersection(curve.start, curve.end, other.start, other.end, epsilon)
1646 .into_iter()
1647 .collect()
1648 }
1649 (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => other
1650 .center
1651 .map(|other_center| {
1652 line_arc_intersections(curve.start, curve.end, other_center, other.start, other.end, epsilon)
1653 .into_iter()
1654 .map(|(_, point)| point)
1655 .collect()
1656 })
1657 .unwrap_or_default(),
1658 (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
1659 let Some(other_center) = other.center else {
1660 return Vec::new();
1661 };
1662 let other_radius = other.radius.unwrap_or_else(|| {
1663 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1664 });
1665 line_circle_intersections(curve.start, curve.end, other_center, other_radius, epsilon)
1666 .into_iter()
1667 .map(|(_, point)| point)
1668 .collect()
1669 }
1670 (CurveKind::Circular, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => curve
1671 .center
1672 .map(|curve_center| {
1673 line_arc_intersections(other.start, other.end, curve_center, curve.start, curve.end, epsilon)
1674 .into_iter()
1675 .map(|(_, point)| point)
1676 .collect()
1677 })
1678 .unwrap_or_default(),
1679 (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => {
1680 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1681 return Vec::new();
1682 };
1683 arc_arc_intersections(
1684 curve_center,
1685 curve.start,
1686 curve.end,
1687 other_center,
1688 other.start,
1689 other.end,
1690 epsilon,
1691 )
1692 }
1693 (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
1694 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1695 return Vec::new();
1696 };
1697 let other_radius = other.radius.unwrap_or_else(|| {
1698 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1699 });
1700 circle_arc_intersections(
1701 other_center,
1702 other_radius,
1703 curve_center,
1704 curve.start,
1705 curve.end,
1706 epsilon,
1707 )
1708 }
1709 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Line, CurveDomain::Open) => {
1710 let Some(curve_center) = curve.center else {
1711 return Vec::new();
1712 };
1713 let curve_radius = curve.radius.unwrap_or_else(|| {
1714 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1715 });
1716 line_circle_intersections(other.start, other.end, curve_center, curve_radius, epsilon)
1717 .into_iter()
1718 .map(|(_, point)| point)
1719 .collect()
1720 }
1721 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Open) => {
1722 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1723 return Vec::new();
1724 };
1725 let curve_radius = curve.radius.unwrap_or_else(|| {
1726 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1727 });
1728 circle_arc_intersections(
1729 curve_center,
1730 curve_radius,
1731 other_center,
1732 other.start,
1733 other.end,
1734 epsilon,
1735 )
1736 }
1737 (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Closed) => {
1738 let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1739 return Vec::new();
1740 };
1741 let curve_radius = curve.radius.unwrap_or_else(|| {
1742 ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1743 });
1744 let other_radius = other.radius.unwrap_or_else(|| {
1745 ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1746 });
1747 circle_circle_intersections(curve_center, curve_radius, other_center, other_radius, epsilon)
1748 }
1749 _ => Vec::new(),
1750 }
1751}
1752
1753fn segment_endpoint_points(
1754 segment_obj: &Object,
1755 objects: &[Object],
1756 default_unit: UnitLength,
1757) -> Vec<(ObjectId, Coords2d)> {
1758 let ObjectKind::Segment { segment } = &segment_obj.kind else {
1759 return Vec::new();
1760 };
1761
1762 match segment {
1763 Segment::Line(line) => {
1764 let mut points = Vec::new();
1765 if let Some(start) = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit) {
1766 points.push((line.start, start));
1767 }
1768 if let Some(end) = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit) {
1769 points.push((line.end, end));
1770 }
1771 points
1772 }
1773 Segment::Arc(arc) => {
1774 let mut points = Vec::new();
1775 if let Some(start) = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit) {
1776 points.push((arc.start, start));
1777 }
1778 if let Some(end) = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit) {
1779 points.push((arc.end, end));
1780 }
1781 points
1782 }
1783 _ => Vec::new(),
1784 }
1785}
1786
1787pub fn get_next_trim_spawn(
1815 points: &[Coords2d],
1816 start_index: usize,
1817 objects: &[Object],
1818 default_unit: UnitLength,
1819) -> TrimItem {
1820 let scene_curves: Vec<CurveHandle> = objects
1821 .iter()
1822 .filter_map(|obj| load_curve_handle(obj, objects, default_unit).ok())
1823 .collect();
1824
1825 for i in start_index..points.len().saturating_sub(1) {
1827 let p1 = points[i];
1828 let p2 = points[i + 1];
1829
1830 for curve in &scene_curves {
1832 let intersections = curve_line_segment_intersections(*curve, p1, p2, EPSILON_POINT_ON_SEGMENT);
1833 if let Some((_, intersection)) = intersections.first() {
1834 return TrimItem::Spawn {
1835 trim_spawn_seg_id: curve.segment_id,
1836 trim_spawn_coords: *intersection,
1837 next_index: i,
1838 };
1839 }
1840 }
1841 }
1842
1843 TrimItem::None {
1845 next_index: points.len().saturating_sub(1),
1846 }
1847}
1848
1849pub fn get_trim_spawn_terminations(
1904 trim_spawn_seg_id: ObjectId,
1905 trim_spawn_coords: &[Coords2d],
1906 objects: &[Object],
1907 default_unit: UnitLength,
1908) -> Result<TrimTerminations, String> {
1909 let trim_spawn_seg = objects.iter().find(|obj| obj.id == trim_spawn_seg_id);
1911
1912 let trim_spawn_seg = match trim_spawn_seg {
1913 Some(seg) => seg,
1914 None => {
1915 return Err(format!("Trim spawn segment {} not found", trim_spawn_seg_id.0));
1916 }
1917 };
1918
1919 let trim_curve = load_curve_handle(trim_spawn_seg, objects, default_unit).map_err(|e| {
1920 format!(
1921 "Failed to load trim spawn segment {} as normalized curve: {}",
1922 trim_spawn_seg_id.0, e
1923 )
1924 })?;
1925
1926 let all_intersections = curve_polyline_intersections(trim_curve, trim_spawn_coords, EPSILON_POINT_ON_SEGMENT);
1931
1932 let intersection_point = if all_intersections.is_empty() {
1935 return Err("Could not find intersection point between polyline and trim spawn segment".to_string());
1936 } else {
1937 let mid_index = (trim_spawn_coords.len() - 1) / 2;
1939 let mid_point = trim_spawn_coords[mid_index];
1940
1941 let mut min_dist = f64::INFINITY;
1943 let mut closest_intersection = all_intersections[0].0;
1944
1945 for (intersection, _) in &all_intersections {
1946 let dist = ((intersection.x - mid_point.x) * (intersection.x - mid_point.x)
1947 + (intersection.y - mid_point.y) * (intersection.y - mid_point.y))
1948 .sqrt();
1949 if dist < min_dist {
1950 min_dist = dist;
1951 closest_intersection = *intersection;
1952 }
1953 }
1954
1955 closest_intersection
1956 };
1957
1958 let intersection_t = project_point_onto_curve(trim_curve, intersection_point)?;
1960
1961 let left_termination = find_termination_in_direction(
1963 trim_spawn_seg,
1964 trim_curve,
1965 intersection_t,
1966 TrimDirection::Left,
1967 objects,
1968 default_unit,
1969 )?;
1970
1971 let right_termination = find_termination_in_direction(
1972 trim_spawn_seg,
1973 trim_curve,
1974 intersection_t,
1975 TrimDirection::Right,
1976 objects,
1977 default_unit,
1978 )?;
1979
1980 Ok(TrimTerminations {
1981 left_side: left_termination,
1982 right_side: right_termination,
1983 })
1984}
1985
1986fn find_termination_in_direction(
2039 trim_spawn_seg: &Object,
2040 trim_curve: CurveHandle,
2041 intersection_t: f64,
2042 direction: TrimDirection,
2043 objects: &[Object],
2044 default_unit: UnitLength,
2045) -> Result<TrimTermination, String> {
2046 let ObjectKind::Segment { segment } = &trim_spawn_seg.kind else {
2048 return Err("Trim spawn segment is not a segment".to_string());
2049 };
2050
2051 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2053 enum CandidateType {
2054 Intersection,
2055 Coincident,
2056 Endpoint,
2057 }
2058
2059 #[derive(Debug, Clone)]
2060 struct Candidate {
2061 t: f64,
2062 point: Coords2d,
2063 candidate_type: CandidateType,
2064 segment_id: Option<ObjectId>,
2065 point_id: Option<ObjectId>,
2066 }
2067
2068 let mut candidates: Vec<Candidate> = Vec::new();
2069
2070 match segment {
2072 Segment::Line(line) => {
2073 candidates.push(Candidate {
2074 t: 0.0,
2075 point: trim_curve.start,
2076 candidate_type: CandidateType::Endpoint,
2077 segment_id: None,
2078 point_id: Some(line.start),
2079 });
2080 candidates.push(Candidate {
2081 t: 1.0,
2082 point: trim_curve.end,
2083 candidate_type: CandidateType::Endpoint,
2084 segment_id: None,
2085 point_id: Some(line.end),
2086 });
2087 }
2088 Segment::Arc(arc) => {
2089 candidates.push(Candidate {
2091 t: 0.0,
2092 point: trim_curve.start,
2093 candidate_type: CandidateType::Endpoint,
2094 segment_id: None,
2095 point_id: Some(arc.start),
2096 });
2097 candidates.push(Candidate {
2098 t: 1.0,
2099 point: trim_curve.end,
2100 candidate_type: CandidateType::Endpoint,
2101 segment_id: None,
2102 point_id: Some(arc.end),
2103 });
2104 }
2105 Segment::Circle(_) => {
2106 }
2108 _ => {}
2109 }
2110
2111 let trim_spawn_seg_id = trim_spawn_seg.id;
2113
2114 for other_seg in objects.iter() {
2116 let other_id = other_seg.id;
2117 if other_id == trim_spawn_seg_id {
2118 continue;
2119 }
2120
2121 if let Ok(other_curve) = load_curve_handle(other_seg, objects, default_unit) {
2122 for intersection in curve_curve_intersections(trim_curve, other_curve, EPSILON_POINT_ON_SEGMENT) {
2123 let Ok(t) = project_point_onto_curve(trim_curve, intersection) else {
2124 continue;
2125 };
2126 candidates.push(Candidate {
2127 t,
2128 point: intersection,
2129 candidate_type: CandidateType::Intersection,
2130 segment_id: Some(other_id),
2131 point_id: None,
2132 });
2133 }
2134 }
2135
2136 for (other_point_id, other_point) in segment_endpoint_points(other_seg, objects, default_unit) {
2137 if !is_point_coincident_with_segment_native(other_point_id, trim_spawn_seg_id, objects) {
2138 continue;
2139 }
2140 if !curve_contains_point(trim_curve, other_point, EPSILON_POINT_ON_SEGMENT) {
2141 continue;
2142 }
2143 let Ok(t) = project_point_onto_curve(trim_curve, other_point) else {
2144 continue;
2145 };
2146 candidates.push(Candidate {
2147 t,
2148 point: other_point,
2149 candidate_type: CandidateType::Coincident,
2150 segment_id: Some(other_id),
2151 point_id: Some(other_point_id),
2152 });
2153 }
2154 }
2155
2156 let is_circle_segment = trim_curve.domain == CurveDomain::Closed;
2157
2158 let intersection_epsilon = EPSILON_POINT_ON_SEGMENT * 10.0; let direction_distance = |candidate_t: f64| -> f64 {
2162 if is_circle_segment {
2163 match direction {
2164 TrimDirection::Left => (intersection_t - candidate_t).rem_euclid(1.0),
2165 TrimDirection::Right => (candidate_t - intersection_t).rem_euclid(1.0),
2166 }
2167 } else {
2168 (candidate_t - intersection_t).abs()
2169 }
2170 };
2171 let filtered_candidates: Vec<Candidate> = candidates
2172 .into_iter()
2173 .filter(|candidate| {
2174 let dist_from_intersection = if is_circle_segment {
2175 let ccw = (candidate.t - intersection_t).rem_euclid(1.0);
2176 let cw = (intersection_t - candidate.t).rem_euclid(1.0);
2177 libm::fmin(ccw, cw)
2178 } else {
2179 (candidate.t - intersection_t).abs()
2180 };
2181 if dist_from_intersection < intersection_epsilon {
2182 return false; }
2184
2185 if is_circle_segment {
2186 direction_distance(candidate.t) > intersection_epsilon
2187 } else {
2188 match direction {
2189 TrimDirection::Left => candidate.t < intersection_t,
2190 TrimDirection::Right => candidate.t > intersection_t,
2191 }
2192 }
2193 })
2194 .collect();
2195
2196 let mut sorted_candidates = filtered_candidates;
2202 sorted_candidates.sort_by(|a, b| {
2203 let dist_a = direction_distance(a.t);
2204 let dist_b = direction_distance(b.t);
2205 let dist_diff = dist_a - dist_b;
2206 let coincident_snap_applies = dist_diff.abs() <= EPSILON_COINCIDENT_TERMINATION_SNAP
2207 && (a.candidate_type == CandidateType::Coincident || b.candidate_type == CandidateType::Coincident);
2208 if dist_diff.abs() > EPSILON_POINT_ON_SEGMENT && !coincident_snap_applies {
2209 dist_diff.partial_cmp(&0.0).unwrap_or(std::cmp::Ordering::Equal)
2210 } else {
2211 let type_priority = |candidate_type: CandidateType| -> i32 {
2213 match candidate_type {
2214 CandidateType::Coincident => 0,
2215 CandidateType::Intersection => 1,
2216 CandidateType::Endpoint => 2,
2217 }
2218 };
2219 type_priority(a.candidate_type).cmp(&type_priority(b.candidate_type))
2220 }
2221 });
2222
2223 let closest_candidate = match sorted_candidates.first() {
2225 Some(c) => c,
2226 None => {
2227 if is_circle_segment {
2228 return Err("No trim termination candidate found for circle".to_string());
2229 }
2230 let endpoint = match direction {
2232 TrimDirection::Left => trim_curve.start,
2233 TrimDirection::Right => trim_curve.end,
2234 };
2235 return Ok(TrimTermination::SegEndPoint {
2236 trim_termination_coords: endpoint,
2237 });
2238 }
2239 };
2240
2241 if !is_circle_segment
2245 && closest_candidate.candidate_type == CandidateType::Intersection
2246 && let Some(seg_id) = closest_candidate.segment_id
2247 {
2248 let intersecting_seg = objects.iter().find(|obj| obj.id == seg_id);
2249
2250 if let Some(intersecting_seg) = intersecting_seg {
2251 let endpoint_epsilon = EPSILON_POINT_ON_SEGMENT * 1000.0; let is_other_seg_endpoint = segment_endpoint_points(intersecting_seg, objects, default_unit)
2254 .into_iter()
2255 .any(|(_, endpoint)| {
2256 let dist_to_endpoint = ((closest_candidate.point.x - endpoint.x).powi(2)
2257 + (closest_candidate.point.y - endpoint.y).powi(2))
2258 .sqrt();
2259 dist_to_endpoint < endpoint_epsilon
2260 });
2261
2262 if is_other_seg_endpoint {
2265 let endpoint = match direction {
2266 TrimDirection::Left => trim_curve.start,
2267 TrimDirection::Right => trim_curve.end,
2268 };
2269 return Ok(TrimTermination::SegEndPoint {
2270 trim_termination_coords: endpoint,
2271 });
2272 }
2273 }
2274
2275 let endpoint_t = match direction {
2277 TrimDirection::Left => 0.0,
2278 TrimDirection::Right => 1.0,
2279 };
2280 let endpoint = match direction {
2281 TrimDirection::Left => trim_curve.start,
2282 TrimDirection::Right => trim_curve.end,
2283 };
2284 let dist_to_endpoint_param = (closest_candidate.t - endpoint_t).abs();
2285 let dist_to_endpoint_coords = ((closest_candidate.point.x - endpoint.x)
2286 * (closest_candidate.point.x - endpoint.x)
2287 + (closest_candidate.point.y - endpoint.y) * (closest_candidate.point.y - endpoint.y))
2288 .sqrt();
2289
2290 let is_at_endpoint =
2291 dist_to_endpoint_param < EPSILON_POINT_ON_SEGMENT || dist_to_endpoint_coords < EPSILON_POINT_ON_SEGMENT;
2292
2293 if is_at_endpoint {
2294 return Ok(TrimTermination::SegEndPoint {
2296 trim_termination_coords: endpoint,
2297 });
2298 }
2299 }
2300
2301 let endpoint_t_for_return = match direction {
2303 TrimDirection::Left => 0.0,
2304 TrimDirection::Right => 1.0,
2305 };
2306 if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Intersection {
2307 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2308 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2309 let endpoint = match direction {
2312 TrimDirection::Left => trim_curve.start,
2313 TrimDirection::Right => trim_curve.end,
2314 };
2315 return Ok(TrimTermination::SegEndPoint {
2316 trim_termination_coords: endpoint,
2317 });
2318 }
2319 }
2320
2321 let endpoint = match direction {
2323 TrimDirection::Left => trim_curve.start,
2324 TrimDirection::Right => trim_curve.end,
2325 };
2326 if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Endpoint {
2327 let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2328 if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2329 return Ok(TrimTermination::SegEndPoint {
2331 trim_termination_coords: endpoint,
2332 });
2333 }
2334 }
2335
2336 if closest_candidate.candidate_type == CandidateType::Coincident {
2338 Ok(TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2340 trim_termination_coords: closest_candidate.point,
2341 intersecting_seg_id: closest_candidate
2342 .segment_id
2343 .ok_or_else(|| "Missing segment_id for coincident".to_string())?,
2344 other_segment_point_id: closest_candidate
2345 .point_id
2346 .ok_or_else(|| "Missing point_id for coincident".to_string())?,
2347 })
2348 } else if closest_candidate.candidate_type == CandidateType::Intersection {
2349 Ok(TrimTermination::Intersection {
2350 trim_termination_coords: closest_candidate.point,
2351 intersecting_seg_id: closest_candidate
2352 .segment_id
2353 .ok_or_else(|| "Missing segment_id for intersection".to_string())?,
2354 })
2355 } else {
2356 if is_circle_segment {
2357 return Err("Circle trim termination unexpectedly resolved to endpoint".to_string());
2358 }
2359 Ok(TrimTermination::SegEndPoint {
2361 trim_termination_coords: closest_candidate.point,
2362 })
2363 }
2364}
2365
2366#[cfg(test)]
2377#[allow(dead_code)]
2378pub(crate) async fn execute_trim_loop<F, Fut>(
2379 points: &[Coords2d],
2380 default_unit: UnitLength,
2381 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2382 mut execute_operations: F,
2383) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>
2384where
2385 F: FnMut(Vec<TrimOperation>, crate::frontend::api::SceneGraphDelta) -> Fut,
2386 Fut: std::future::Future<
2387 Output = Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>,
2388 >,
2389{
2390 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2392 let points = normalized_points.as_slice();
2393
2394 let mut start_index = 0;
2395 let max_iterations = 1000;
2396 let mut iteration_count = 0;
2397 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2398 crate::frontend::api::SourceDelta { text: String::new() },
2399 initial_scene_graph_delta.clone(),
2400 ));
2401 let mut invalidates_ids = false;
2402 let mut current_scene_graph_delta = initial_scene_graph_delta;
2403 let circle_delete_fallback_strategy =
2404 |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
2405 if !error.contains("No trim termination candidate found for circle") {
2406 return None;
2407 }
2408 let is_circle = scene_objects
2409 .iter()
2410 .find(|obj| obj.id == segment_id)
2411 .is_some_and(|obj| {
2412 matches!(
2413 obj.kind,
2414 ObjectKind::Segment {
2415 segment: Segment::Circle(_)
2416 }
2417 )
2418 });
2419 if is_circle {
2420 Some(vec![TrimOperation::SimpleTrim {
2421 segment_to_trim_id: segment_id,
2422 }])
2423 } else {
2424 None
2425 }
2426 };
2427
2428 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2429 iteration_count += 1;
2430
2431 let next_trim_spawn = get_next_trim_spawn(
2433 points,
2434 start_index,
2435 ¤t_scene_graph_delta.new_graph.objects,
2436 default_unit,
2437 );
2438
2439 match &next_trim_spawn {
2440 TrimItem::None { next_index } => {
2441 let old_start_index = start_index;
2442 start_index = *next_index;
2443
2444 if start_index <= old_start_index {
2446 start_index = old_start_index + 1;
2447 }
2448
2449 if start_index >= points.len().saturating_sub(1) {
2451 break;
2452 }
2453 continue;
2454 }
2455 TrimItem::Spawn {
2456 trim_spawn_seg_id,
2457 trim_spawn_coords,
2458 next_index,
2459 ..
2460 } => {
2461 let terminations = match get_trim_spawn_terminations(
2463 *trim_spawn_seg_id,
2464 points,
2465 ¤t_scene_graph_delta.new_graph.objects,
2466 default_unit,
2467 ) {
2468 Ok(terms) => terms,
2469 Err(e) => {
2470 crate::logln!("Error getting trim spawn terminations: {}", e);
2471 if let Some(strategy) = circle_delete_fallback_strategy(
2472 &e,
2473 *trim_spawn_seg_id,
2474 ¤t_scene_graph_delta.new_graph.objects,
2475 ) {
2476 match execute_operations(strategy, current_scene_graph_delta.clone()).await {
2477 Ok((source_delta, scene_graph_delta)) => {
2478 last_result = Some((source_delta, scene_graph_delta.clone()));
2479 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2480 current_scene_graph_delta = scene_graph_delta;
2481 }
2482 Err(exec_err) => {
2483 crate::logln!(
2484 "Error executing circle-delete fallback trim operation: {}",
2485 exec_err
2486 );
2487 }
2488 }
2489
2490 let old_start_index = start_index;
2491 start_index = *next_index;
2492 if start_index <= old_start_index {
2493 start_index = old_start_index + 1;
2494 }
2495 continue;
2496 }
2497
2498 let old_start_index = start_index;
2499 start_index = *next_index;
2500 if start_index <= old_start_index {
2501 start_index = old_start_index + 1;
2502 }
2503 continue;
2504 }
2505 };
2506
2507 let trim_spawn_segment = current_scene_graph_delta
2509 .new_graph
2510 .objects
2511 .iter()
2512 .find(|obj| obj.id == *trim_spawn_seg_id)
2513 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
2514
2515 let plan = match build_trim_plan(
2516 *trim_spawn_seg_id,
2517 *trim_spawn_coords,
2518 trim_spawn_segment,
2519 &terminations.left_side,
2520 &terminations.right_side,
2521 ¤t_scene_graph_delta.new_graph.objects,
2522 default_unit,
2523 ) {
2524 Ok(plan) => plan,
2525 Err(e) => {
2526 crate::logln!("Error determining trim strategy: {}", e);
2527 let old_start_index = start_index;
2528 start_index = *next_index;
2529 if start_index <= old_start_index {
2530 start_index = old_start_index + 1;
2531 }
2532 continue;
2533 }
2534 };
2535 let strategy = lower_trim_plan(&plan);
2536
2537 let geometry_was_modified = trim_plan_modifies_geometry(&plan);
2540
2541 match execute_operations(strategy, current_scene_graph_delta.clone()).await {
2543 Ok((source_delta, scene_graph_delta)) => {
2544 last_result = Some((source_delta, scene_graph_delta.clone()));
2545 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2546 current_scene_graph_delta = scene_graph_delta;
2547 }
2548 Err(e) => {
2549 crate::logln!("Error executing trim operations: {}", e);
2550 }
2552 }
2553
2554 let old_start_index = start_index;
2556 start_index = *next_index;
2557
2558 if start_index <= old_start_index && !geometry_was_modified {
2560 start_index = old_start_index + 1;
2561 }
2562 }
2563 }
2564 }
2565
2566 if iteration_count >= max_iterations {
2567 return Err(format!("Reached max iterations ({})", max_iterations));
2568 }
2569
2570 last_result.ok_or_else(|| "No trim operations were executed".to_string())
2572}
2573
2574#[cfg(all(feature = "artifact-graph", test))]
2576#[derive(Debug, Clone)]
2577pub struct TrimFlowResult {
2578 pub kcl_code: String,
2579 pub invalidates_ids: bool,
2580}
2581
2582#[cfg(all(not(target_arch = "wasm32"), feature = "artifact-graph", test))]
2598pub(crate) async fn execute_trim_flow(
2599 kcl_code: &str,
2600 trim_points: &[Coords2d],
2601 sketch_id: ObjectId,
2602) -> Result<TrimFlowResult, String> {
2603 use crate::ExecutorContext;
2604 use crate::Program;
2605 use crate::execution::MockConfig;
2606 use crate::frontend::FrontendState;
2607 use crate::frontend::api::Version;
2608
2609 let parse_result = Program::parse(kcl_code).map_err(|e| format!("Failed to parse KCL: {}", e))?;
2611 let (program_opt, errors) = parse_result;
2612 if !errors.is_empty() {
2613 return Err(format!("Failed to parse KCL: {:?}", errors));
2614 }
2615 let program = program_opt.ok_or_else(|| "No AST produced".to_string())?;
2616
2617 let mock_ctx = ExecutorContext::new_mock(None).await;
2618
2619 let result = async {
2621 let mut frontend = FrontendState::new();
2622
2623 frontend.program = program.clone();
2625
2626 let exec_outcome = mock_ctx
2627 .run_mock(&program, &MockConfig::default())
2628 .await
2629 .map_err(|e| format!("Failed to execute program: {}", e.error.message()))?;
2630
2631 let exec_outcome = frontend.update_state_after_exec(exec_outcome, false);
2632 #[allow(unused_mut)] let mut initial_scene_graph = frontend.scene_graph.clone();
2634
2635 #[cfg(feature = "artifact-graph")]
2638 if initial_scene_graph.objects.is_empty() && !exec_outcome.scene_objects.is_empty() {
2639 initial_scene_graph.objects = exec_outcome.scene_objects.clone();
2640 }
2641
2642 let actual_sketch_id = if let Some(sketch_mode) = initial_scene_graph.sketch_mode {
2645 sketch_mode
2646 } else {
2647 initial_scene_graph
2649 .objects
2650 .iter()
2651 .find(|obj| matches!(obj.kind, crate::frontend::api::ObjectKind::Sketch { .. }))
2652 .map(|obj| obj.id)
2653 .unwrap_or(sketch_id) };
2655
2656 let version = Version(0);
2657 let initial_scene_graph_delta = crate::frontend::api::SceneGraphDelta {
2658 new_graph: initial_scene_graph,
2659 new_objects: vec![],
2660 invalidates_ids: false,
2661 exec_outcome,
2662 };
2663
2664 let (source_delta, scene_graph_delta) = execute_trim_loop_with_context(
2669 trim_points,
2670 initial_scene_graph_delta,
2671 &mut frontend,
2672 &mock_ctx,
2673 version,
2674 actual_sketch_id,
2675 )
2676 .await?;
2677
2678 if source_delta.text.is_empty() {
2681 return Err("No trim operations were executed - source delta is empty".to_string());
2682 }
2683
2684 Ok(TrimFlowResult {
2685 kcl_code: source_delta.text,
2686 invalidates_ids: scene_graph_delta.invalidates_ids,
2687 })
2688 }
2689 .await;
2690
2691 mock_ctx.close().await;
2693
2694 result
2695}
2696
2697pub async fn execute_trim_loop_with_context(
2703 points: &[Coords2d],
2704 initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2705 frontend: &mut crate::frontend::FrontendState,
2706 ctx: &crate::ExecutorContext,
2707 version: crate::frontend::api::Version,
2708 sketch_id: ObjectId,
2709) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
2710 let default_unit = frontend.default_length_unit();
2712 let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2713
2714 let mut current_scene_graph_delta = initial_scene_graph_delta.clone();
2717 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2718 crate::frontend::api::SourceDelta { text: String::new() },
2719 initial_scene_graph_delta.clone(),
2720 ));
2721 let mut invalidates_ids = false;
2722 let mut start_index = 0;
2723 let max_iterations = 1000;
2724 let mut iteration_count = 0;
2725 let circle_delete_fallback_strategy =
2726 |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
2727 if !error.contains("No trim termination candidate found for circle") {
2728 return None;
2729 }
2730 let is_circle = scene_objects
2731 .iter()
2732 .find(|obj| obj.id == segment_id)
2733 .is_some_and(|obj| {
2734 matches!(
2735 obj.kind,
2736 ObjectKind::Segment {
2737 segment: Segment::Circle(_)
2738 }
2739 )
2740 });
2741 if is_circle {
2742 Some(vec![TrimOperation::SimpleTrim {
2743 segment_to_trim_id: segment_id,
2744 }])
2745 } else {
2746 None
2747 }
2748 };
2749
2750 let points = normalized_points.as_slice();
2751
2752 while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2753 iteration_count += 1;
2754
2755 let next_trim_spawn = get_next_trim_spawn(
2757 points,
2758 start_index,
2759 ¤t_scene_graph_delta.new_graph.objects,
2760 default_unit,
2761 );
2762
2763 match &next_trim_spawn {
2764 TrimItem::None { next_index } => {
2765 let old_start_index = start_index;
2766 start_index = *next_index;
2767 if start_index <= old_start_index {
2768 start_index = old_start_index + 1;
2769 }
2770 if start_index >= points.len().saturating_sub(1) {
2771 break;
2772 }
2773 continue;
2774 }
2775 TrimItem::Spawn {
2776 trim_spawn_seg_id,
2777 trim_spawn_coords,
2778 next_index,
2779 ..
2780 } => {
2781 let terminations = match get_trim_spawn_terminations(
2783 *trim_spawn_seg_id,
2784 points,
2785 ¤t_scene_graph_delta.new_graph.objects,
2786 default_unit,
2787 ) {
2788 Ok(terms) => terms,
2789 Err(e) => {
2790 crate::logln!("Error getting trim spawn terminations: {}", e);
2791 if let Some(strategy) = circle_delete_fallback_strategy(
2792 &e,
2793 *trim_spawn_seg_id,
2794 ¤t_scene_graph_delta.new_graph.objects,
2795 ) {
2796 match execute_trim_operations_simple(
2797 strategy.clone(),
2798 ¤t_scene_graph_delta,
2799 frontend,
2800 ctx,
2801 version,
2802 sketch_id,
2803 )
2804 .await
2805 {
2806 Ok((source_delta, scene_graph_delta)) => {
2807 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2808 last_result = Some((source_delta, scene_graph_delta.clone()));
2809 current_scene_graph_delta = scene_graph_delta;
2810 }
2811 Err(exec_err) => {
2812 crate::logln!(
2813 "Error executing circle-delete fallback trim operation: {}",
2814 exec_err
2815 );
2816 }
2817 }
2818
2819 let old_start_index = start_index;
2820 start_index = *next_index;
2821 if start_index <= old_start_index {
2822 start_index = old_start_index + 1;
2823 }
2824 continue;
2825 }
2826
2827 let old_start_index = start_index;
2828 start_index = *next_index;
2829 if start_index <= old_start_index {
2830 start_index = old_start_index + 1;
2831 }
2832 continue;
2833 }
2834 };
2835
2836 let trim_spawn_segment = current_scene_graph_delta
2838 .new_graph
2839 .objects
2840 .iter()
2841 .find(|obj| obj.id == *trim_spawn_seg_id)
2842 .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
2843
2844 let plan = match build_trim_plan(
2845 *trim_spawn_seg_id,
2846 *trim_spawn_coords,
2847 trim_spawn_segment,
2848 &terminations.left_side,
2849 &terminations.right_side,
2850 ¤t_scene_graph_delta.new_graph.objects,
2851 default_unit,
2852 ) {
2853 Ok(plan) => plan,
2854 Err(e) => {
2855 crate::logln!("Error determining trim strategy: {}", e);
2856 let old_start_index = start_index;
2857 start_index = *next_index;
2858 if start_index <= old_start_index {
2859 start_index = old_start_index + 1;
2860 }
2861 continue;
2862 }
2863 };
2864 let strategy = lower_trim_plan(&plan);
2865
2866 let geometry_was_modified = trim_plan_modifies_geometry(&plan);
2869
2870 match execute_trim_operations_simple(
2872 strategy.clone(),
2873 ¤t_scene_graph_delta,
2874 frontend,
2875 ctx,
2876 version,
2877 sketch_id,
2878 )
2879 .await
2880 {
2881 Ok((source_delta, scene_graph_delta)) => {
2882 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2883 last_result = Some((source_delta, scene_graph_delta.clone()));
2884 current_scene_graph_delta = scene_graph_delta;
2885 }
2886 Err(e) => {
2887 crate::logln!("Error executing trim operations: {}", e);
2888 }
2889 }
2890
2891 let old_start_index = start_index;
2893 start_index = *next_index;
2894 if start_index <= old_start_index && !geometry_was_modified {
2895 start_index = old_start_index + 1;
2896 }
2897 }
2898 }
2899 }
2900
2901 if iteration_count >= max_iterations {
2902 return Err(format!("Reached max iterations ({})", max_iterations));
2903 }
2904
2905 let (source_delta, mut scene_graph_delta) =
2906 last_result.ok_or_else(|| "No trim operations were executed".to_string())?;
2907 scene_graph_delta.invalidates_ids = invalidates_ids;
2909 Ok((source_delta, scene_graph_delta))
2910}
2911
2912pub(crate) fn build_trim_plan(
2972 trim_spawn_id: ObjectId,
2973 trim_spawn_coords: Coords2d,
2974 trim_spawn_segment: &Object,
2975 left_side: &TrimTermination,
2976 right_side: &TrimTermination,
2977 objects: &[Object],
2978 default_unit: UnitLength,
2979) -> Result<TrimPlan, String> {
2980 if matches!(left_side, TrimTermination::SegEndPoint { .. })
2982 && matches!(right_side, TrimTermination::SegEndPoint { .. })
2983 {
2984 return Ok(TrimPlan::DeleteSegment {
2985 segment_id: trim_spawn_id,
2986 });
2987 }
2988
2989 let is_intersect_or_coincident = |side: &TrimTermination| -> bool {
2991 matches!(
2992 side,
2993 TrimTermination::Intersection { .. }
2994 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
2995 )
2996 };
2997
2998 let left_side_needs_tail_cut = is_intersect_or_coincident(left_side) && !is_intersect_or_coincident(right_side);
2999 let right_side_needs_tail_cut = is_intersect_or_coincident(right_side) && !is_intersect_or_coincident(left_side);
3000
3001 let ObjectKind::Segment { segment } = &trim_spawn_segment.kind else {
3003 return Err("Trim spawn segment is not a segment".to_string());
3004 };
3005
3006 let (_segment_type, ctor) = match segment {
3007 Segment::Line(line) => ("Line", &line.ctor),
3008 Segment::Arc(arc) => ("Arc", &arc.ctor),
3009 Segment::Circle(circle) => ("Circle", &circle.ctor),
3010 _ => {
3011 return Err("Trim spawn segment is not a Line, Arc, or Circle".to_string());
3012 }
3013 };
3014
3015 let units = match ctor {
3017 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
3018 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
3019 _ => NumericSuffix::Mm,
3020 },
3021 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
3022 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
3023 _ => NumericSuffix::Mm,
3024 },
3025 SegmentCtor::Circle(circle_ctor) => match &circle_ctor.start.x {
3026 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
3027 _ => NumericSuffix::Mm,
3028 },
3029 _ => NumericSuffix::Mm,
3030 };
3031
3032 let find_distance_constraints_for_segment = |segment_id: ObjectId| -> Vec<ObjectId> {
3034 let mut constraint_ids = Vec::new();
3035 for obj in objects {
3036 let ObjectKind::Constraint { constraint } = &obj.kind else {
3037 continue;
3038 };
3039
3040 let Constraint::Distance(distance) = constraint else {
3041 continue;
3042 };
3043
3044 let points_owned_by_segment: Vec<bool> = distance
3050 .point_ids()
3051 .map(|point_id| {
3052 if let Some(point_obj) = objects.iter().find(|o| o.id == point_id)
3053 && let ObjectKind::Segment { segment } = &point_obj.kind
3054 && let Segment::Point(point) = segment
3055 && let Some(owner_id) = point.owner
3056 {
3057 return owner_id == segment_id;
3058 }
3059 false
3060 })
3061 .collect();
3062
3063 if points_owned_by_segment.len() == 2 && points_owned_by_segment.iter().all(|&owned| owned) {
3065 constraint_ids.push(obj.id);
3066 }
3067 }
3068 constraint_ids
3069 };
3070
3071 let find_existing_point_segment_coincident =
3073 |trim_seg_id: ObjectId, intersecting_seg_id: ObjectId| -> CoincidentData {
3074 let lookup_by_point_id = |point_id: ObjectId| -> Option<CoincidentData> {
3076 for obj in objects {
3077 let ObjectKind::Constraint { constraint } = &obj.kind else {
3078 continue;
3079 };
3080
3081 let Constraint::Coincident(coincident) = constraint else {
3082 continue;
3083 };
3084
3085 let involves_trim_seg = coincident.segment_ids().any(|id| id == trim_seg_id || id == point_id);
3086 let involves_point = coincident.contains_segment(point_id);
3087
3088 if involves_trim_seg && involves_point {
3089 return Some(CoincidentData {
3090 intersecting_seg_id,
3091 intersecting_endpoint_point_id: Some(point_id),
3092 existing_point_segment_constraint_id: Some(obj.id),
3093 });
3094 }
3095 }
3096 None
3097 };
3098
3099 let trim_seg = objects.iter().find(|obj| obj.id == trim_seg_id);
3101
3102 let mut trim_endpoint_ids: Vec<ObjectId> = Vec::new();
3103 if let Some(seg) = trim_seg
3104 && let ObjectKind::Segment { segment } = &seg.kind
3105 {
3106 match segment {
3107 Segment::Line(line) => {
3108 trim_endpoint_ids.push(line.start);
3109 trim_endpoint_ids.push(line.end);
3110 }
3111 Segment::Arc(arc) => {
3112 trim_endpoint_ids.push(arc.start);
3113 trim_endpoint_ids.push(arc.end);
3114 }
3115 _ => {}
3116 }
3117 }
3118
3119 let intersecting_obj = objects.iter().find(|obj| obj.id == intersecting_seg_id);
3120
3121 if let Some(obj) = intersecting_obj
3122 && let ObjectKind::Segment { segment } = &obj.kind
3123 && let Segment::Point(_) = segment
3124 && let Some(found) = lookup_by_point_id(intersecting_seg_id)
3125 {
3126 return found;
3127 }
3128
3129 let mut intersecting_endpoint_ids: Vec<ObjectId> = Vec::new();
3131 if let Some(obj) = intersecting_obj
3132 && let ObjectKind::Segment { segment } = &obj.kind
3133 {
3134 match segment {
3135 Segment::Line(line) => {
3136 intersecting_endpoint_ids.push(line.start);
3137 intersecting_endpoint_ids.push(line.end);
3138 }
3139 Segment::Arc(arc) => {
3140 intersecting_endpoint_ids.push(arc.start);
3141 intersecting_endpoint_ids.push(arc.end);
3142 }
3143 _ => {}
3144 }
3145 }
3146
3147 intersecting_endpoint_ids.push(intersecting_seg_id);
3149
3150 for obj in objects {
3152 let ObjectKind::Constraint { constraint } = &obj.kind else {
3153 continue;
3154 };
3155
3156 let Constraint::Coincident(coincident) = constraint else {
3157 continue;
3158 };
3159
3160 let constraint_segment_ids: Vec<ObjectId> = coincident.get_segments();
3161
3162 let involves_trim_seg = constraint_segment_ids.contains(&trim_seg_id)
3164 || trim_endpoint_ids.iter().any(|&id| constraint_segment_ids.contains(&id));
3165
3166 if !involves_trim_seg {
3167 continue;
3168 }
3169
3170 if let Some(&intersecting_endpoint_id) = intersecting_endpoint_ids
3172 .iter()
3173 .find(|&&id| constraint_segment_ids.contains(&id))
3174 {
3175 return CoincidentData {
3176 intersecting_seg_id,
3177 intersecting_endpoint_point_id: Some(intersecting_endpoint_id),
3178 existing_point_segment_constraint_id: Some(obj.id),
3179 };
3180 }
3181 }
3182
3183 CoincidentData {
3185 intersecting_seg_id,
3186 intersecting_endpoint_point_id: None,
3187 existing_point_segment_constraint_id: None,
3188 }
3189 };
3190
3191 let find_point_segment_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<serde_json::Value> {
3193 let mut constraints: Vec<serde_json::Value> = Vec::new();
3194 for obj in objects {
3195 let ObjectKind::Constraint { constraint } = &obj.kind else {
3196 continue;
3197 };
3198
3199 let Constraint::Coincident(coincident) = constraint else {
3200 continue;
3201 };
3202
3203 if !coincident.contains_segment(endpoint_point_id) {
3205 continue;
3206 }
3207
3208 let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
3210
3211 if let Some(other_id) = other_segment_id
3212 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
3213 {
3214 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
3216 constraints.push(serde_json::json!({
3217 "constraintId": obj.id.0,
3218 "segmentOrPointId": other_id.0,
3219 }));
3220 }
3221 }
3222 }
3223 constraints
3224 };
3225
3226 let find_point_point_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
3229 let mut constraint_ids = Vec::new();
3230 for obj in objects {
3231 let ObjectKind::Constraint { constraint } = &obj.kind else {
3232 continue;
3233 };
3234
3235 let Constraint::Coincident(coincident) = constraint else {
3236 continue;
3237 };
3238
3239 if !coincident.contains_segment(endpoint_point_id) {
3241 continue;
3242 }
3243
3244 let is_point_point = coincident.segment_ids().all(|seg_id| {
3246 if let Some(seg_obj) = objects.iter().find(|o| o.id == seg_id) {
3247 matches!(&seg_obj.kind, ObjectKind::Segment { segment } if matches!(segment, Segment::Point(_)))
3248 } else {
3249 false
3250 }
3251 });
3252
3253 if is_point_point {
3254 constraint_ids.push(obj.id);
3255 }
3256 }
3257 constraint_ids
3258 };
3259
3260 let find_point_segment_coincident_constraint_ids = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
3263 let mut constraint_ids = Vec::new();
3264 for obj in objects {
3265 let ObjectKind::Constraint { constraint } = &obj.kind else {
3266 continue;
3267 };
3268
3269 let Constraint::Coincident(coincident) = constraint else {
3270 continue;
3271 };
3272
3273 if !coincident.contains_segment(endpoint_point_id) {
3275 continue;
3276 }
3277
3278 let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
3280
3281 if let Some(other_id) = other_segment_id
3282 && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
3283 {
3284 if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
3286 constraint_ids.push(obj.id);
3287 }
3288 }
3289 }
3290 constraint_ids
3291 };
3292
3293 let find_midpoint_constraints_for_segment = |segment_id: ObjectId| -> Vec<ObjectId> {
3294 objects
3295 .iter()
3296 .filter_map(|obj| {
3297 let ObjectKind::Constraint { constraint } = &obj.kind else {
3298 return None;
3299 };
3300
3301 let Constraint::Midpoint(midpoint) = constraint else {
3302 return None;
3303 };
3304
3305 (midpoint.segment == segment_id).then_some(obj.id)
3306 })
3307 .collect()
3308 };
3309
3310 if left_side_needs_tail_cut || right_side_needs_tail_cut {
3312 let side = if left_side_needs_tail_cut {
3313 left_side
3314 } else {
3315 right_side
3316 };
3317
3318 let intersection_coords = match side {
3319 TrimTermination::Intersection {
3320 trim_termination_coords,
3321 ..
3322 }
3323 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3324 trim_termination_coords,
3325 ..
3326 } => *trim_termination_coords,
3327 TrimTermination::SegEndPoint { .. } => {
3328 return Err("Logic error: side should not be segEndPoint here".to_string());
3329 }
3330 };
3331
3332 let endpoint_to_change = if left_side_needs_tail_cut {
3333 EndpointChanged::End
3334 } else {
3335 EndpointChanged::Start
3336 };
3337
3338 let intersecting_seg_id = match side {
3339 TrimTermination::Intersection {
3340 intersecting_seg_id, ..
3341 }
3342 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3343 intersecting_seg_id, ..
3344 } => *intersecting_seg_id,
3345 TrimTermination::SegEndPoint { .. } => {
3346 return Err("Logic error".to_string());
3347 }
3348 };
3349
3350 let mut coincident_data = if matches!(
3351 side,
3352 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3353 ) {
3354 let point_id = match side {
3355 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3356 other_segment_point_id, ..
3357 } => *other_segment_point_id,
3358 _ => return Err("Logic error".to_string()),
3359 };
3360 let mut data = find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id);
3361 data.intersecting_endpoint_point_id = Some(point_id);
3362 data
3363 } else {
3364 find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id)
3365 };
3366
3367 if matches!(side, TrimTermination::Intersection { .. })
3368 && let Some(point_id) = coincident_data.intersecting_endpoint_point_id
3369 {
3370 let endpoint_is_at_intersection = get_point_coords_from_native(objects, point_id, default_unit)
3371 .is_some_and(|point_coords| {
3372 ((point_coords.x - intersection_coords.x).powi(2)
3373 + (point_coords.y - intersection_coords.y).powi(2))
3374 .sqrt()
3375 <= EPSILON_POINT_ON_SEGMENT * 1000.0
3376 });
3377
3378 if !endpoint_is_at_intersection {
3379 coincident_data.existing_point_segment_constraint_id = None;
3380 coincident_data.intersecting_endpoint_point_id = None;
3381 }
3382 }
3383
3384 let trim_seg = objects.iter().find(|obj| obj.id == trim_spawn_id);
3386
3387 let endpoint_point_id = if let Some(seg) = trim_seg {
3388 let ObjectKind::Segment { segment } = &seg.kind else {
3389 return Err("Trim spawn segment is not a segment".to_string());
3390 };
3391 match segment {
3392 Segment::Line(line) => {
3393 if endpoint_to_change == EndpointChanged::Start {
3394 Some(line.start)
3395 } else {
3396 Some(line.end)
3397 }
3398 }
3399 Segment::Arc(arc) => {
3400 if endpoint_to_change == EndpointChanged::Start {
3401 Some(arc.start)
3402 } else {
3403 Some(arc.end)
3404 }
3405 }
3406 _ => None,
3407 }
3408 } else {
3409 None
3410 };
3411
3412 if let (Some(endpoint_id), Some(existing_constraint_id)) =
3413 (endpoint_point_id, coincident_data.existing_point_segment_constraint_id)
3414 {
3415 let constraint_involves_trimmed_endpoint = objects
3416 .iter()
3417 .find(|obj| obj.id == existing_constraint_id)
3418 .and_then(|obj| match &obj.kind {
3419 ObjectKind::Constraint {
3420 constraint: Constraint::Coincident(coincident),
3421 } => Some(coincident.contains_segment(endpoint_id) || coincident.contains_segment(trim_spawn_id)),
3422 _ => None,
3423 })
3424 .unwrap_or(false);
3425
3426 if !constraint_involves_trimmed_endpoint {
3427 coincident_data.existing_point_segment_constraint_id = None;
3428 coincident_data.intersecting_endpoint_point_id = None;
3429 }
3430 }
3431
3432 let coincident_end_constraint_to_delete_ids = if let Some(point_id) = endpoint_point_id {
3434 let mut constraint_ids = find_point_point_coincident_constraints(point_id);
3435 constraint_ids.extend(find_point_segment_coincident_constraint_ids(point_id));
3437 constraint_ids
3438 } else {
3439 Vec::new()
3440 };
3441
3442 let point_axis_constraint_ids_to_delete = if let Some(point_id) = endpoint_point_id {
3443 objects
3444 .iter()
3445 .filter_map(|obj| {
3446 let ObjectKind::Constraint { constraint } = &obj.kind else {
3447 return None;
3448 };
3449
3450 point_axis_constraint_references_point(constraint, point_id).then_some(obj.id)
3451 })
3452 .collect::<Vec<_>>()
3453 } else {
3454 Vec::new()
3455 };
3456
3457 let new_ctor = match ctor {
3459 SegmentCtor::Line(line_ctor) => {
3460 let new_point = crate::frontend::sketch::Point2d {
3462 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
3463 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
3464 };
3465 if endpoint_to_change == EndpointChanged::Start {
3466 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3467 start: new_point,
3468 end: line_ctor.end.clone(),
3469 construction: line_ctor.construction,
3470 })
3471 } else {
3472 SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3473 start: line_ctor.start.clone(),
3474 end: new_point,
3475 construction: line_ctor.construction,
3476 })
3477 }
3478 }
3479 SegmentCtor::Arc(arc_ctor) => {
3480 let new_point = crate::frontend::sketch::Point2d {
3482 x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
3483 y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
3484 };
3485 if endpoint_to_change == EndpointChanged::Start {
3486 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3487 start: new_point,
3488 end: arc_ctor.end.clone(),
3489 center: arc_ctor.center.clone(),
3490 construction: arc_ctor.construction,
3491 })
3492 } else {
3493 SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3494 start: arc_ctor.start.clone(),
3495 end: new_point,
3496 center: arc_ctor.center.clone(),
3497 construction: arc_ctor.construction,
3498 })
3499 }
3500 }
3501 _ => {
3502 return Err("Unsupported segment type for edit".to_string());
3503 }
3504 };
3505
3506 let mut all_constraint_ids_to_delete: Vec<ObjectId> = Vec::new();
3508 if let Some(constraint_id) = coincident_data.existing_point_segment_constraint_id {
3509 all_constraint_ids_to_delete.push(constraint_id);
3510 }
3511 all_constraint_ids_to_delete.extend(coincident_end_constraint_to_delete_ids);
3512 all_constraint_ids_to_delete.extend(point_axis_constraint_ids_to_delete);
3513 all_constraint_ids_to_delete.extend(find_midpoint_constraints_for_segment(trim_spawn_id));
3514
3515 let distance_constraint_ids = find_distance_constraints_for_segment(trim_spawn_id);
3518 all_constraint_ids_to_delete.extend(distance_constraint_ids);
3519
3520 let coincident_target_id = coincident_data
3521 .intersecting_endpoint_point_id
3522 .unwrap_or(intersecting_seg_id);
3523 let adds_curved_segment_coincident = endpoint_point_id
3524 .is_some_and(|point_id| segment_id_is_or_is_owned_by_curve(objects, point_id))
3525 || segment_id_is_or_is_owned_by_curve(objects, coincident_target_id);
3526 let has_midpoint_deletions = all_constraint_ids_to_delete.iter().any(|constraint_id| {
3527 objects
3528 .iter()
3529 .find(|obj| obj.id == *constraint_id)
3530 .is_some_and(|object| {
3531 matches!(
3532 object.kind,
3533 ObjectKind::Constraint {
3534 constraint: Constraint::Midpoint(_)
3535 }
3536 )
3537 })
3538 });
3539
3540 let mut additional_edited_segment_ids = IndexSet::new();
3541 if has_midpoint_deletions || (adds_curved_segment_coincident && all_constraint_ids_to_delete.is_empty()) {
3542 additional_edited_segment_ids.extend(sketch_segment_ids_for_segment(objects, trim_spawn_id));
3543 }
3544
3545 if adds_curved_segment_coincident {
3546 for constraint_id in &all_constraint_ids_to_delete {
3547 let Some(constraint_object) = objects.iter().find(|obj| obj.id == *constraint_id) else {
3548 continue;
3549 };
3550 let ObjectKind::Constraint {
3551 constraint: Constraint::Coincident(coincident),
3552 } = &constraint_object.kind
3553 else {
3554 continue;
3555 };
3556
3557 additional_edited_segment_ids.extend(
3558 coincident
3559 .segment_ids()
3560 .map(|segment_id| owner_or_segment_id(objects, segment_id)),
3561 );
3562 }
3563 }
3564
3565 return Ok(TrimPlan::TailCut {
3566 segment_id: trim_spawn_id,
3567 endpoint_changed: endpoint_to_change,
3568 ctor: new_ctor,
3569 segment_or_point_to_make_coincident_to: intersecting_seg_id,
3570 intersecting_endpoint_point_id: coincident_data.intersecting_endpoint_point_id,
3571 constraint_ids_to_delete: all_constraint_ids_to_delete,
3572 additional_edited_segment_ids: additional_edited_segment_ids.into_iter().collect(),
3573 });
3574 }
3575
3576 if matches!(segment, Segment::Circle(_)) {
3579 let left_side_intersects = is_intersect_or_coincident(left_side);
3580 let right_side_intersects = is_intersect_or_coincident(right_side);
3581 if !(left_side_intersects && right_side_intersects) {
3582 return Err(format!(
3583 "Unsupported circle trim termination combination: left={:?} right={:?}",
3584 left_side, right_side
3585 ));
3586 }
3587
3588 let left_trim_coords = match left_side {
3589 TrimTermination::SegEndPoint {
3590 trim_termination_coords,
3591 }
3592 | TrimTermination::Intersection {
3593 trim_termination_coords,
3594 ..
3595 }
3596 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3597 trim_termination_coords,
3598 ..
3599 } => *trim_termination_coords,
3600 };
3601 let right_trim_coords = match right_side {
3602 TrimTermination::SegEndPoint {
3603 trim_termination_coords,
3604 }
3605 | TrimTermination::Intersection {
3606 trim_termination_coords,
3607 ..
3608 }
3609 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3610 trim_termination_coords,
3611 ..
3612 } => *trim_termination_coords,
3613 };
3614
3615 let trim_points_coincident = ((left_trim_coords.x - right_trim_coords.x)
3618 * (left_trim_coords.x - right_trim_coords.x)
3619 + (left_trim_coords.y - right_trim_coords.y) * (left_trim_coords.y - right_trim_coords.y))
3620 .sqrt()
3621 <= EPSILON_POINT_ON_SEGMENT * 10.0;
3622 if trim_points_coincident {
3623 return Ok(TrimPlan::DeleteSegment {
3624 segment_id: trim_spawn_id,
3625 });
3626 }
3627
3628 let circle_center_coords =
3629 get_position_coords_from_circle(trim_spawn_segment, CirclePoint::Center, objects, default_unit)
3630 .ok_or_else(|| {
3631 format!(
3632 "Could not get center coordinates for circle segment {}",
3633 trim_spawn_id.0
3634 )
3635 })?;
3636
3637 let spawn_on_left_to_right = is_point_on_arc(
3639 trim_spawn_coords,
3640 circle_center_coords,
3641 left_trim_coords,
3642 right_trim_coords,
3643 EPSILON_POINT_ON_SEGMENT,
3644 );
3645 let (arc_start_coords, arc_end_coords, arc_start_termination, arc_end_termination) = if spawn_on_left_to_right {
3646 (
3647 right_trim_coords,
3648 left_trim_coords,
3649 Box::new(right_side.clone()),
3650 Box::new(left_side.clone()),
3651 )
3652 } else {
3653 (
3654 left_trim_coords,
3655 right_trim_coords,
3656 Box::new(left_side.clone()),
3657 Box::new(right_side.clone()),
3658 )
3659 };
3660
3661 return Ok(TrimPlan::ReplaceCircleWithArc {
3662 circle_id: trim_spawn_id,
3663 arc_start_coords,
3664 arc_end_coords,
3665 arc_start_termination,
3666 arc_end_termination,
3667 });
3668 }
3669
3670 let left_side_intersects = is_intersect_or_coincident(left_side);
3672 let right_side_intersects = is_intersect_or_coincident(right_side);
3673
3674 if left_side_intersects && right_side_intersects {
3675 let left_intersecting_seg_id = match left_side {
3678 TrimTermination::Intersection {
3679 intersecting_seg_id, ..
3680 }
3681 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3682 intersecting_seg_id, ..
3683 } => *intersecting_seg_id,
3684 TrimTermination::SegEndPoint { .. } => {
3685 return Err("Logic error: left side should not be segEndPoint".to_string());
3686 }
3687 };
3688
3689 let right_intersecting_seg_id = match right_side {
3690 TrimTermination::Intersection {
3691 intersecting_seg_id, ..
3692 }
3693 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3694 intersecting_seg_id, ..
3695 } => *intersecting_seg_id,
3696 TrimTermination::SegEndPoint { .. } => {
3697 return Err("Logic error: right side should not be segEndPoint".to_string());
3698 }
3699 };
3700
3701 let left_coincident_data = if matches!(
3702 left_side,
3703 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3704 ) {
3705 let point_id = match left_side {
3706 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3707 other_segment_point_id, ..
3708 } => *other_segment_point_id,
3709 _ => return Err("Logic error".to_string()),
3710 };
3711 let mut data = find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id);
3712 data.intersecting_endpoint_point_id = Some(point_id);
3713 data
3714 } else {
3715 find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id)
3716 };
3717
3718 let right_coincident_data = if matches!(
3719 right_side,
3720 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3721 ) {
3722 let point_id = match right_side {
3723 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3724 other_segment_point_id, ..
3725 } => *other_segment_point_id,
3726 _ => return Err("Logic error".to_string()),
3727 };
3728 let mut data = find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id);
3729 data.intersecting_endpoint_point_id = Some(point_id);
3730 data
3731 } else {
3732 find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id)
3733 };
3734
3735 let (original_start_point_id, original_end_point_id) = match segment {
3737 Segment::Line(line) => (Some(line.start), Some(line.end)),
3738 Segment::Arc(arc) => (Some(arc.start), Some(arc.end)),
3739 _ => (None, None),
3740 };
3741
3742 let original_end_point_coords = match segment {
3744 Segment::Line(_) => {
3745 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
3746 }
3747 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
3748 _ => None,
3749 };
3750
3751 let Some(original_end_coords) = original_end_point_coords else {
3752 return Err(
3753 "Could not get original end point coordinates before editing - this is required for split trim"
3754 .to_string(),
3755 );
3756 };
3757
3758 let left_trim_coords = match left_side {
3760 TrimTermination::SegEndPoint {
3761 trim_termination_coords,
3762 }
3763 | TrimTermination::Intersection {
3764 trim_termination_coords,
3765 ..
3766 }
3767 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3768 trim_termination_coords,
3769 ..
3770 } => *trim_termination_coords,
3771 };
3772
3773 let right_trim_coords = match right_side {
3774 TrimTermination::SegEndPoint {
3775 trim_termination_coords,
3776 }
3777 | TrimTermination::Intersection {
3778 trim_termination_coords,
3779 ..
3780 }
3781 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3782 trim_termination_coords,
3783 ..
3784 } => *trim_termination_coords,
3785 };
3786
3787 let dist_to_original_end = ((right_trim_coords.x - original_end_coords.x)
3789 * (right_trim_coords.x - original_end_coords.x)
3790 + (right_trim_coords.y - original_end_coords.y) * (right_trim_coords.y - original_end_coords.y))
3791 .sqrt();
3792 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3793 return Err(
3794 "Split point is at original end point - this should be handled as cutTail, not split".to_string(),
3795 );
3796 }
3797
3798 let mut constraints_to_migrate: Vec<ConstraintToMigrate> = Vec::new();
3801 let mut constraints_to_delete_set: IndexSet<ObjectId> = IndexSet::new();
3802
3803 if let Some(constraint_id) = left_coincident_data.existing_point_segment_constraint_id {
3805 constraints_to_delete_set.insert(constraint_id);
3806 }
3807 if let Some(constraint_id) = right_coincident_data.existing_point_segment_constraint_id {
3808 constraints_to_delete_set.insert(constraint_id);
3809 }
3810
3811 if let Some(end_id) = original_end_point_id {
3812 for obj in objects {
3813 let ObjectKind::Constraint { constraint } = &obj.kind else {
3814 continue;
3815 };
3816
3817 if point_axis_constraint_references_point(constraint, end_id) {
3818 constraints_to_delete_set.insert(obj.id);
3819 }
3820 }
3821 }
3822
3823 if let Some(end_id) = original_end_point_id {
3825 let end_point_point_constraint_ids = find_point_point_coincident_constraints(end_id);
3826 for constraint_id in end_point_point_constraint_ids {
3827 let other_point_id_opt = objects.iter().find_map(|obj| {
3829 if obj.id != constraint_id {
3830 return None;
3831 }
3832 let ObjectKind::Constraint { constraint } = &obj.kind else {
3833 return None;
3834 };
3835 let Constraint::Coincident(coincident) = constraint else {
3836 return None;
3837 };
3838 coincident.segment_ids().find(|&seg_id| seg_id != end_id)
3839 });
3840
3841 if let Some(other_point_id) = other_point_id_opt {
3842 constraints_to_delete_set.insert(constraint_id);
3843 constraints_to_migrate.push(ConstraintToMigrate {
3845 constraint_id,
3846 other_entity_id: other_point_id,
3847 is_point_point: true,
3848 attach_to_endpoint: AttachToEndpoint::End,
3849 });
3850 }
3851 }
3852 }
3853
3854 if let Some(end_id) = original_end_point_id {
3856 let end_point_segment_constraints = find_point_segment_coincident_constraints(end_id);
3857 for constraint_json in end_point_segment_constraints {
3858 if let Some(constraint_id_usize) = constraint_json
3859 .get("constraintId")
3860 .and_then(|v| v.as_u64())
3861 .map(|id| id as usize)
3862 {
3863 let constraint_id = ObjectId(constraint_id_usize);
3864 constraints_to_delete_set.insert(constraint_id);
3865 if let Some(other_id_usize) = constraint_json
3867 .get("segmentOrPointId")
3868 .and_then(|v| v.as_u64())
3869 .map(|id| id as usize)
3870 {
3871 constraints_to_migrate.push(ConstraintToMigrate {
3872 constraint_id,
3873 other_entity_id: ObjectId(other_id_usize),
3874 is_point_point: false,
3875 attach_to_endpoint: AttachToEndpoint::End,
3876 });
3877 }
3878 }
3879 }
3880 }
3881
3882 if let Some(end_id) = original_end_point_id {
3887 for obj in objects {
3888 let ObjectKind::Constraint { constraint } = &obj.kind else {
3889 continue;
3890 };
3891
3892 let Constraint::Coincident(coincident) = constraint else {
3893 continue;
3894 };
3895
3896 if !coincident.contains_segment(trim_spawn_id) {
3901 continue;
3902 }
3903 if let (Some(start_id), Some(end_id_val)) = (original_start_point_id, Some(end_id))
3906 && coincident.segment_ids().any(|id| id == start_id || id == end_id_val)
3907 {
3908 continue; }
3910
3911 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
3913
3914 if let Some(other_id) = other_id {
3915 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3917 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3918 continue;
3919 };
3920
3921 let Segment::Point(point) = other_segment else {
3922 continue;
3923 };
3924
3925 let point_coords = Coords2d {
3927 x: number_to_unit(&point.position.x, default_unit),
3928 y: number_to_unit(&point.position.y, default_unit),
3929 };
3930
3931 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
3934 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
3935 if let ObjectKind::Segment {
3936 segment: Segment::Point(end_point),
3937 } = &end_point_obj.kind
3938 {
3939 Some(Coords2d {
3940 x: number_to_unit(&end_point.position.x, default_unit),
3941 y: number_to_unit(&end_point.position.y, default_unit),
3942 })
3943 } else {
3944 None
3945 }
3946 } else {
3947 None
3948 }
3949 } else {
3950 None
3951 };
3952
3953 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
3954 let dist_to_original_end = ((point_coords.x - reference_coords.x)
3955 * (point_coords.x - reference_coords.x)
3956 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
3957 .sqrt();
3958
3959 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3960 let has_point_point_constraint = find_point_point_coincident_constraints(end_id)
3963 .iter()
3964 .any(|&constraint_id| {
3965 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
3966 if let ObjectKind::Constraint {
3967 constraint: Constraint::Coincident(coincident),
3968 } = &constraint_obj.kind
3969 {
3970 coincident.contains_segment(other_id)
3971 } else {
3972 false
3973 }
3974 } else {
3975 false
3976 }
3977 });
3978
3979 if !has_point_point_constraint {
3980 constraints_to_migrate.push(ConstraintToMigrate {
3982 constraint_id: obj.id,
3983 other_entity_id: other_id,
3984 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
3987 }
3988 constraints_to_delete_set.insert(obj.id);
3990 }
3991 }
3992 }
3993 }
3994 }
3995
3996 let split_point = right_trim_coords; let segment_start_coords = match segment {
4001 Segment::Line(_) => {
4002 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::Start, objects, default_unit)
4003 }
4004 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Start, objects, default_unit),
4005 _ => None,
4006 };
4007 let segment_end_coords = match segment {
4008 Segment::Line(_) => {
4009 get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
4010 }
4011 Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
4012 _ => None,
4013 };
4014 let segment_center_coords = match segment {
4015 Segment::Line(_) => None,
4016 Segment::Arc(_) => {
4017 get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Center, objects, default_unit)
4018 }
4019 _ => None,
4020 };
4021
4022 if let (Some(start_coords), Some(end_coords)) = (segment_start_coords, segment_end_coords) {
4023 let split_point_t_opt = match segment {
4025 Segment::Line(_) => Some(project_point_onto_segment(split_point, start_coords, end_coords)),
4026 Segment::Arc(_) => segment_center_coords
4027 .map(|center| project_point_onto_arc(split_point, center, start_coords, end_coords)),
4028 _ => None,
4029 };
4030
4031 if let Some(split_point_t) = split_point_t_opt {
4032 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 let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id)
4049 && coincident.segment_ids().any(|id| id == start_id || id == end_id)
4050 {
4051 continue;
4052 }
4053
4054 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
4056
4057 if let Some(other_id) = other_id {
4058 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
4060 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
4061 continue;
4062 };
4063
4064 let Segment::Point(point) = other_segment else {
4065 continue;
4066 };
4067
4068 let point_coords = Coords2d {
4070 x: number_to_unit(&point.position.x, default_unit),
4071 y: number_to_unit(&point.position.y, default_unit),
4072 };
4073
4074 let point_t = match segment {
4076 Segment::Line(_) => project_point_onto_segment(point_coords, start_coords, end_coords),
4077 Segment::Arc(_) => {
4078 if let Some(center) = segment_center_coords {
4079 project_point_onto_arc(point_coords, center, start_coords, end_coords)
4080 } else {
4081 continue; }
4083 }
4084 _ => continue, };
4086
4087 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
4090 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
4091 if let ObjectKind::Segment {
4092 segment: Segment::Point(end_point),
4093 } = &end_point_obj.kind
4094 {
4095 Some(Coords2d {
4096 x: number_to_unit(&end_point.position.x, default_unit),
4097 y: number_to_unit(&end_point.position.y, default_unit),
4098 })
4099 } else {
4100 None
4101 }
4102 } else {
4103 None
4104 }
4105 } else {
4106 None
4107 };
4108
4109 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
4110 let dist_to_original_end = ((point_coords.x - reference_coords.x)
4111 * (point_coords.x - reference_coords.x)
4112 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
4113 .sqrt();
4114
4115 if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
4116 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
4120 find_point_point_coincident_constraints(end_id)
4121 .iter()
4122 .any(|&constraint_id| {
4123 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
4124 {
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 continue; }
4154
4155 let dist_to_start = ((point_coords.x - start_coords.x) * (point_coords.x - start_coords.x)
4157 + (point_coords.y - start_coords.y) * (point_coords.y - start_coords.y))
4158 .sqrt();
4159 let is_at_start = (point_t - 0.0).abs() < EPSILON_POINT_ON_SEGMENT
4160 || dist_to_start < EPSILON_POINT_ON_SEGMENT;
4161
4162 if is_at_start {
4163 continue; }
4165
4166 let dist_to_split = (point_t - split_point_t).abs();
4168 if dist_to_split < EPSILON_POINT_ON_SEGMENT * 100.0 {
4169 continue; }
4171
4172 if point_t > split_point_t {
4174 constraints_to_migrate.push(ConstraintToMigrate {
4175 constraint_id: obj.id,
4176 other_entity_id: other_id,
4177 is_point_point: false, attach_to_endpoint: AttachToEndpoint::Segment, });
4180 constraints_to_delete_set.insert(obj.id);
4181 }
4182 }
4183 }
4184 }
4185 } } let distance_constraint_ids_for_split = find_distance_constraints_for_segment(trim_spawn_id);
4193
4194 let arc_center_point_id: Option<ObjectId> = match segment {
4196 Segment::Arc(arc) => Some(arc.center),
4197 _ => None,
4198 };
4199
4200 for constraint_id in distance_constraint_ids_for_split {
4201 if let Some(center_id) = arc_center_point_id {
4203 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
4205 && let ObjectKind::Constraint { constraint } = &constraint_obj.kind
4206 && let Constraint::Distance(distance) = constraint
4207 && distance.contains_point(center_id)
4208 {
4209 continue;
4211 }
4212 }
4213
4214 constraints_to_delete_set.insert(constraint_id);
4215 }
4216
4217 for obj in objects {
4220 let ObjectKind::Constraint { constraint } = &obj.kind else {
4221 continue;
4222 };
4223
4224 let Constraint::Midpoint(midpoint) = constraint else {
4225 continue;
4226 };
4227
4228 let references_trimmed_segment = midpoint.segment == trim_spawn_id;
4229 let references_trimmed_endpoint = original_start_point_id.is_some_and(|id| midpoint.point == id)
4230 || original_end_point_id.is_some_and(|id| midpoint.point == id);
4231
4232 if references_trimmed_segment || references_trimmed_endpoint {
4233 constraints_to_delete_set.insert(obj.id);
4234 }
4235 }
4236
4237 for obj in objects {
4245 let ObjectKind::Constraint { constraint } = &obj.kind else {
4246 continue;
4247 };
4248
4249 let Constraint::Coincident(coincident) = constraint else {
4250 continue;
4251 };
4252
4253 if !coincident.contains_segment(trim_spawn_id) {
4255 continue;
4256 }
4257
4258 if constraints_to_delete_set.contains(&obj.id) {
4260 continue;
4261 }
4262
4263 let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
4270
4271 if let Some(other_id) = other_id {
4272 if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
4274 let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
4275 continue;
4276 };
4277
4278 let Segment::Point(point) = other_segment else {
4279 continue;
4280 };
4281
4282 let _is_endpoint_constraint =
4285 if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id) {
4286 coincident.segment_ids().any(|id| id == start_id || id == end_id)
4287 } else {
4288 false
4289 };
4290
4291 let point_coords = Coords2d {
4293 x: number_to_unit(&point.position.x, default_unit),
4294 y: number_to_unit(&point.position.y, default_unit),
4295 };
4296
4297 let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
4299 if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
4300 if let ObjectKind::Segment {
4301 segment: Segment::Point(end_point),
4302 } = &end_point_obj.kind
4303 {
4304 Some(Coords2d {
4305 x: number_to_unit(&end_point.position.x, default_unit),
4306 y: number_to_unit(&end_point.position.y, default_unit),
4307 })
4308 } else {
4309 None
4310 }
4311 } else {
4312 None
4313 }
4314 } else {
4315 None
4316 };
4317
4318 let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
4319 let dist_to_original_end = ((point_coords.x - reference_coords.x)
4320 * (point_coords.x - reference_coords.x)
4321 + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
4322 .sqrt();
4323
4324 let is_at_original_end = dist_to_original_end < EPSILON_POINT_ON_SEGMENT * 2.0;
4327
4328 if is_at_original_end {
4329 let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
4332 find_point_point_coincident_constraints(end_id)
4333 .iter()
4334 .any(|&constraint_id| {
4335 if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
4336 if let ObjectKind::Constraint {
4337 constraint: Constraint::Coincident(coincident),
4338 } = &constraint_obj.kind
4339 {
4340 coincident.contains_segment(other_id)
4341 } else {
4342 false
4343 }
4344 } else {
4345 false
4346 }
4347 })
4348 } else {
4349 false
4350 };
4351
4352 if !has_point_point_constraint {
4353 constraints_to_migrate.push(ConstraintToMigrate {
4355 constraint_id: obj.id,
4356 other_entity_id: other_id,
4357 is_point_point: true, attach_to_endpoint: AttachToEndpoint::End, });
4360 }
4361 constraints_to_delete_set.insert(obj.id);
4363 }
4364 }
4365 }
4366 }
4367
4368 let constraints_to_delete: Vec<ObjectId> = constraints_to_delete_set.iter().copied().collect();
4370 let plan = TrimPlan::SplitSegment {
4371 segment_id: trim_spawn_id,
4372 left_trim_coords,
4373 right_trim_coords,
4374 original_end_coords,
4375 left_side: Box::new(left_side.clone()),
4376 right_side: Box::new(right_side.clone()),
4377 left_side_coincident_data: CoincidentData {
4378 intersecting_seg_id: left_intersecting_seg_id,
4379 intersecting_endpoint_point_id: left_coincident_data.intersecting_endpoint_point_id,
4380 existing_point_segment_constraint_id: left_coincident_data.existing_point_segment_constraint_id,
4381 },
4382 right_side_coincident_data: CoincidentData {
4383 intersecting_seg_id: right_intersecting_seg_id,
4384 intersecting_endpoint_point_id: right_coincident_data.intersecting_endpoint_point_id,
4385 existing_point_segment_constraint_id: right_coincident_data.existing_point_segment_constraint_id,
4386 },
4387 constraints_to_migrate,
4388 constraints_to_delete,
4389 };
4390
4391 return Ok(plan);
4392 }
4393
4394 Err(format!(
4399 "Unsupported trim termination combination: left={:?} right={:?}",
4400 left_side, right_side
4401 ))
4402}
4403
4404pub(crate) async fn execute_trim_operations_simple(
4416 strategy: Vec<TrimOperation>,
4417 current_scene_graph_delta: &crate::frontend::api::SceneGraphDelta,
4418 frontend: &mut crate::frontend::FrontendState,
4419 ctx: &crate::ExecutorContext,
4420 version: crate::frontend::api::Version,
4421 sketch_id: ObjectId,
4422) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
4423 use crate::frontend::SketchApi;
4424 use crate::frontend::sketch::Constraint;
4425 use crate::frontend::sketch::ExistingSegmentCtor;
4426 use crate::frontend::sketch::SegmentCtor;
4427
4428 let default_unit = frontend.default_length_unit();
4429
4430 let mut op_index = 0;
4431 let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = None;
4432 let mut invalidates_ids = false;
4433
4434 while op_index < strategy.len() {
4435 let mut consumed_ops = 1;
4436 let operation_result = match &strategy[op_index] {
4437 TrimOperation::SimpleTrim { segment_to_trim_id } => {
4438 frontend
4440 .delete_objects(
4441 ctx,
4442 version,
4443 sketch_id,
4444 Vec::new(), vec![*segment_to_trim_id], )
4447 .await
4448 .map_err(|e| format!("Failed to delete segment: {}", e.error.message()))
4449 }
4450 TrimOperation::EditSegment {
4451 segment_id,
4452 ctor,
4453 endpoint_changed,
4454 additional_edited_segment_ids,
4455 } => {
4456 if op_index + 1 < strategy.len() {
4459 if let TrimOperation::AddCoincidentConstraint {
4460 segment_id: coincident_seg_id,
4461 endpoint_changed: coincident_endpoint_changed,
4462 segment_or_point_to_make_coincident_to,
4463 intersecting_endpoint_point_id,
4464 } = &strategy[op_index + 1]
4465 {
4466 if segment_id == coincident_seg_id && endpoint_changed == coincident_endpoint_changed {
4467 let mut delete_constraint_ids: Vec<ObjectId> = Vec::new();
4469 consumed_ops = 2;
4470
4471 if op_index + 2 < strategy.len()
4472 && let TrimOperation::DeleteConstraints { constraint_ids } = &strategy[op_index + 2]
4473 {
4474 delete_constraint_ids = constraint_ids.to_vec();
4475 consumed_ops = 3;
4476 }
4477
4478 let segment_ctor = ctor.clone();
4480
4481 let edited_segment = current_scene_graph_delta
4483 .new_graph
4484 .objects
4485 .iter()
4486 .find(|obj| obj.id == *segment_id)
4487 .ok_or_else(|| format!("Failed to find segment {} for tail-cut batch", segment_id.0))?;
4488
4489 let endpoint_point_id = match &edited_segment.kind {
4490 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4491 crate::frontend::sketch::Segment::Line(line) => {
4492 if *endpoint_changed == EndpointChanged::Start {
4493 line.start
4494 } else {
4495 line.end
4496 }
4497 }
4498 crate::frontend::sketch::Segment::Arc(arc) => {
4499 if *endpoint_changed == EndpointChanged::Start {
4500 arc.start
4501 } else {
4502 arc.end
4503 }
4504 }
4505 _ => {
4506 return Err("Unsupported segment type for tail-cut batch".to_string());
4507 }
4508 },
4509 _ => {
4510 return Err("Edited object is not a segment (tail-cut batch)".to_string());
4511 }
4512 };
4513
4514 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
4515 vec![endpoint_point_id.into(), (*point_id).into()]
4516 } else {
4517 vec![
4518 endpoint_point_id.into(),
4519 (*segment_or_point_to_make_coincident_to).into(),
4520 ]
4521 };
4522
4523 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4524 segments: coincident_segments,
4525 });
4526
4527 let segment_to_edit = ExistingSegmentCtor {
4528 id: *segment_id,
4529 ctor: segment_ctor,
4530 };
4531
4532 frontend
4535 .batch_tail_cut_operations(
4536 ctx,
4537 version,
4538 sketch_id,
4539 vec![segment_to_edit],
4540 vec![constraint],
4541 delete_constraint_ids,
4542 additional_edited_segment_ids.clone(),
4543 )
4544 .await
4545 .map_err(|e| format!("Failed to batch tail-cut operations: {}", e.error.message()))
4546 } else {
4547 let segment_to_edit = ExistingSegmentCtor {
4549 id: *segment_id,
4550 ctor: ctor.clone(),
4551 };
4552
4553 frontend
4554 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4555 .await
4556 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4557 }
4558 } else {
4559 let segment_to_edit = ExistingSegmentCtor {
4561 id: *segment_id,
4562 ctor: ctor.clone(),
4563 };
4564
4565 frontend
4566 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4567 .await
4568 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4569 }
4570 } else {
4571 let segment_to_edit = ExistingSegmentCtor {
4573 id: *segment_id,
4574 ctor: ctor.clone(),
4575 };
4576
4577 frontend
4578 .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4579 .await
4580 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4581 }
4582 }
4583 TrimOperation::AddCoincidentConstraint {
4584 segment_id,
4585 endpoint_changed,
4586 segment_or_point_to_make_coincident_to,
4587 intersecting_endpoint_point_id,
4588 } => {
4589 let edited_segment = current_scene_graph_delta
4591 .new_graph
4592 .objects
4593 .iter()
4594 .find(|obj| obj.id == *segment_id)
4595 .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
4596
4597 let new_segment_endpoint_point_id = match &edited_segment.kind {
4599 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4600 crate::frontend::sketch::Segment::Line(line) => {
4601 if *endpoint_changed == EndpointChanged::Start {
4602 line.start
4603 } else {
4604 line.end
4605 }
4606 }
4607 crate::frontend::sketch::Segment::Arc(arc) => {
4608 if *endpoint_changed == EndpointChanged::Start {
4609 arc.start
4610 } else {
4611 arc.end
4612 }
4613 }
4614 _ => {
4615 return Err("Unsupported segment type for addCoincidentConstraint".to_string());
4616 }
4617 },
4618 _ => {
4619 return Err("Edited object is not a segment".to_string());
4620 }
4621 };
4622
4623 let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
4625 vec![new_segment_endpoint_point_id.into(), (*point_id).into()]
4626 } else {
4627 vec![
4628 new_segment_endpoint_point_id.into(),
4629 (*segment_or_point_to_make_coincident_to).into(),
4630 ]
4631 };
4632
4633 let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4634 segments: coincident_segments,
4635 });
4636
4637 frontend
4638 .add_constraint(ctx, version, sketch_id, constraint)
4639 .await
4640 .map_err(|e| format!("Failed to add constraint: {}", e.error.message()))
4641 }
4642 TrimOperation::DeleteConstraints { constraint_ids } => {
4643 let constraint_object_ids: Vec<ObjectId> = constraint_ids.to_vec();
4645
4646 frontend
4647 .delete_objects(
4648 ctx,
4649 version,
4650 sketch_id,
4651 constraint_object_ids,
4652 Vec::new(), )
4654 .await
4655 .map_err(|e| format!("Failed to delete constraints: {}", e.error.message()))
4656 }
4657 TrimOperation::ReplaceCircleWithArc {
4658 circle_id,
4659 arc_start_coords,
4660 arc_end_coords,
4661 arc_start_termination,
4662 arc_end_termination,
4663 } => {
4664 let original_circle = current_scene_graph_delta
4666 .new_graph
4667 .objects
4668 .iter()
4669 .find(|obj| obj.id == *circle_id)
4670 .ok_or_else(|| format!("Failed to find original circle {}", circle_id.0))?;
4671
4672 let (original_circle_start_id, original_circle_center_id, circle_ctor) = match &original_circle.kind {
4673 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4674 crate::frontend::sketch::Segment::Circle(circle) => match &circle.ctor {
4675 SegmentCtor::Circle(circle_ctor) => (circle.start, circle.center, circle_ctor.clone()),
4676 _ => return Err("Circle does not have a Circle ctor".to_string()),
4677 },
4678 _ => return Err("Original segment is not a circle".to_string()),
4679 },
4680 _ => return Err("Original object is not a segment".to_string()),
4681 };
4682
4683 let units = match &circle_ctor.start.x {
4684 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4685 _ => crate::pretty::NumericSuffix::Mm,
4686 };
4687
4688 let coords_to_point_expr = |coords: Coords2d| crate::frontend::sketch::Point2d {
4689 x: crate::frontend::api::Expr::Var(unit_to_number(coords.x, default_unit, units)),
4690 y: crate::frontend::api::Expr::Var(unit_to_number(coords.y, default_unit, units)),
4691 };
4692
4693 let arc_ctor = SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4694 start: coords_to_point_expr(*arc_start_coords),
4695 end: coords_to_point_expr(*arc_end_coords),
4696 center: circle_ctor.center.clone(),
4697 construction: circle_ctor.construction,
4698 });
4699
4700 let (_add_source_delta, add_scene_graph_delta) = frontend
4701 .add_segment(ctx, version, sketch_id, arc_ctor, None)
4702 .await
4703 .map_err(|e| format!("Failed to add arc while replacing circle: {}", e.error.message()))?;
4704 invalidates_ids = invalidates_ids || add_scene_graph_delta.invalidates_ids;
4705
4706 let new_arc_id = *add_scene_graph_delta
4707 .new_objects
4708 .iter()
4709 .find(|&id| {
4710 add_scene_graph_delta
4711 .new_graph
4712 .objects
4713 .iter()
4714 .find(|o| o.id == *id)
4715 .is_some_and(|obj| {
4716 matches!(
4717 &obj.kind,
4718 crate::frontend::api::ObjectKind::Segment { segment }
4719 if matches!(segment, crate::frontend::sketch::Segment::Arc(_))
4720 )
4721 })
4722 })
4723 .ok_or_else(|| "Failed to find newly created arc segment".to_string())?;
4724
4725 let new_arc_obj = add_scene_graph_delta
4726 .new_graph
4727 .objects
4728 .iter()
4729 .find(|obj| obj.id == new_arc_id)
4730 .ok_or_else(|| format!("New arc segment not found {}", new_arc_id.0))?;
4731 let (new_arc_start_id, new_arc_end_id, new_arc_center_id) = match &new_arc_obj.kind {
4732 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4733 crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, arc.center),
4734 _ => return Err("New segment is not an arc".to_string()),
4735 },
4736 _ => return Err("New arc object is not a segment".to_string()),
4737 };
4738
4739 let constraint_segments_for =
4740 |arc_endpoint_id: ObjectId,
4741 term: &TrimTermination|
4742 -> Result<Vec<crate::frontend::sketch::ConstraintSegment>, String> {
4743 match term {
4744 TrimTermination::Intersection {
4745 intersecting_seg_id, ..
4746 } => Ok(vec![arc_endpoint_id.into(), (*intersecting_seg_id).into()]),
4747 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4748 other_segment_point_id,
4749 ..
4750 } => Ok(vec![arc_endpoint_id.into(), (*other_segment_point_id).into()]),
4751 TrimTermination::SegEndPoint { .. } => {
4752 Err("Circle replacement endpoint cannot terminate at seg endpoint".to_string())
4753 }
4754 }
4755 };
4756
4757 let start_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4758 segments: constraint_segments_for(new_arc_start_id, arc_start_termination)?,
4759 });
4760 let (_c1_source_delta, c1_scene_graph_delta) = frontend
4761 .add_constraint(ctx, version, sketch_id, start_constraint)
4762 .await
4763 .map_err(|e| format!("Failed to add start coincident on replaced arc: {}", e.error.message()))?;
4764 invalidates_ids = invalidates_ids || c1_scene_graph_delta.invalidates_ids;
4765
4766 let end_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4767 segments: constraint_segments_for(new_arc_end_id, arc_end_termination)?,
4768 });
4769 let (_c2_source_delta, c2_scene_graph_delta) = frontend
4770 .add_constraint(ctx, version, sketch_id, end_constraint)
4771 .await
4772 .map_err(|e| format!("Failed to add end coincident on replaced arc: {}", e.error.message()))?;
4773 invalidates_ids = invalidates_ids || c2_scene_graph_delta.invalidates_ids;
4774
4775 let mut termination_point_ids: Vec<ObjectId> = Vec::new();
4776 for term in [arc_start_termination, arc_end_termination] {
4777 if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4778 other_segment_point_id,
4779 ..
4780 } = term.as_ref()
4781 {
4782 termination_point_ids.push(*other_segment_point_id);
4783 }
4784 }
4785
4786 let rewrite_map = std::collections::HashMap::from([
4790 (*circle_id, new_arc_id),
4791 (original_circle_center_id, new_arc_center_id),
4792 (original_circle_start_id, new_arc_start_id),
4793 ]);
4794 let rewrite_ids: std::collections::HashSet<ObjectId> = rewrite_map.keys().copied().collect();
4795
4796 let mut migrated_constraints: Vec<Constraint> = Vec::new();
4797 for obj in ¤t_scene_graph_delta.new_graph.objects {
4798 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4799 continue;
4800 };
4801
4802 match constraint {
4805 Constraint::Coincident(coincident) => {
4806 if !constraint_segments_reference_any(&coincident.segments, &rewrite_ids) {
4807 continue;
4808 }
4809
4810 if coincident.contains_segment(*circle_id)
4814 && coincident
4815 .segment_ids()
4816 .filter(|id| *id != *circle_id)
4817 .any(|id| termination_point_ids.contains(&id))
4818 {
4819 continue;
4820 }
4821
4822 let Some(Constraint::Coincident(migrated_coincident)) =
4823 rewrite_constraint_with_map(constraint, &rewrite_map)
4824 else {
4825 continue;
4826 };
4827
4828 let migrated_ids: Vec<ObjectId> = migrated_coincident
4832 .segments
4833 .iter()
4834 .filter_map(|segment| match segment {
4835 crate::frontend::sketch::ConstraintSegment::Segment(id) => Some(*id),
4836 crate::frontend::sketch::ConstraintSegment::Origin(_) => None,
4837 })
4838 .collect();
4839 if migrated_ids.contains(&new_arc_id)
4840 && (migrated_ids.contains(&new_arc_start_id) || migrated_ids.contains(&new_arc_end_id))
4841 {
4842 continue;
4843 }
4844
4845 migrated_constraints.push(Constraint::Coincident(migrated_coincident));
4846 }
4847 Constraint::Distance(distance) => {
4848 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4849 continue;
4850 }
4851 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4852 migrated_constraints.push(migrated);
4853 }
4854 }
4855 Constraint::HorizontalDistance(distance) => {
4856 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4857 continue;
4858 }
4859 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4860 migrated_constraints.push(migrated);
4861 }
4862 }
4863 Constraint::VerticalDistance(distance) => {
4864 if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4865 continue;
4866 }
4867 if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4868 migrated_constraints.push(migrated);
4869 }
4870 }
4871 Constraint::Radius(radius) => {
4872 if radius.arc == *circle_id
4873 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4874 {
4875 migrated_constraints.push(migrated);
4876 }
4877 }
4878 Constraint::Diameter(diameter) => {
4879 if diameter.arc == *circle_id
4880 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4881 {
4882 migrated_constraints.push(migrated);
4883 }
4884 }
4885 Constraint::EqualRadius(equal_radius) => {
4886 if equal_radius.input.contains(circle_id)
4887 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4888 {
4889 migrated_constraints.push(migrated);
4890 }
4891 }
4892 Constraint::Tangent(tangent) => {
4893 if tangent.input.contains(circle_id)
4894 && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4895 {
4896 migrated_constraints.push(migrated);
4897 }
4898 }
4899 Constraint::Angle(_)
4900 | Constraint::Fixed(_)
4901 | Constraint::Horizontal(_)
4902 | Constraint::LinesEqualLength(_)
4903 | Constraint::Midpoint(_)
4904 | Constraint::Parallel(_)
4905 | Constraint::Perpendicular(_)
4906 | Constraint::Symmetric(_)
4907 | Constraint::Vertical(_) => {}
4908 }
4909 }
4910
4911 for constraint in migrated_constraints {
4912 let (_source_delta, migrated_scene_graph_delta) = frontend
4913 .add_constraint(ctx, version, sketch_id, constraint)
4914 .await
4915 .map_err(|e| format!("Failed to migrate circle constraint to arc: {}", e.error.message()))?;
4916 invalidates_ids = invalidates_ids || migrated_scene_graph_delta.invalidates_ids;
4917 }
4918
4919 frontend
4920 .delete_objects(ctx, version, sketch_id, Vec::new(), vec![*circle_id])
4921 .await
4922 .map_err(|e| format!("Failed to delete circle after arc replacement: {}", e.error.message()))
4923 }
4924 TrimOperation::SplitSegment {
4925 segment_id,
4926 left_trim_coords,
4927 right_trim_coords,
4928 original_end_coords,
4929 left_side,
4930 right_side,
4931 constraints_to_migrate,
4932 constraints_to_delete,
4933 ..
4934 } => {
4935 let original_segment = current_scene_graph_delta
4940 .new_graph
4941 .objects
4942 .iter()
4943 .find(|obj| obj.id == *segment_id)
4944 .ok_or_else(|| format!("Failed to find original segment {}", segment_id.0))?;
4945
4946 let (original_segment_start_point_id, original_segment_end_point_id, original_segment_center_point_id) =
4948 match &original_segment.kind {
4949 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4950 crate::frontend::sketch::Segment::Line(line) => (Some(line.start), Some(line.end), None),
4951 crate::frontend::sketch::Segment::Arc(arc) => {
4952 (Some(arc.start), Some(arc.end), Some(arc.center))
4953 }
4954 _ => (None, None, None),
4955 },
4956 _ => (None, None, None),
4957 };
4958
4959 let mut center_point_constraints_to_migrate: Vec<(Constraint, ObjectId)> = Vec::new();
4961 if let Some(original_center_id) = original_segment_center_point_id {
4962 for obj in ¤t_scene_graph_delta.new_graph.objects {
4963 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4964 continue;
4965 };
4966
4967 if let Constraint::Coincident(coincident) = constraint
4969 && coincident.contains_segment(original_center_id)
4970 {
4971 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
4972 }
4973
4974 if let Constraint::Distance(distance) = constraint
4976 && distance.contains_point(original_center_id)
4977 {
4978 center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
4979 }
4980 }
4981 }
4982
4983 let (_segment_type, original_ctor) = match &original_segment.kind {
4985 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4986 crate::frontend::sketch::Segment::Line(line) => ("Line", line.ctor.clone()),
4987 crate::frontend::sketch::Segment::Arc(arc) => ("Arc", arc.ctor.clone()),
4988 _ => {
4989 return Err("Original segment is not a Line or Arc".to_string());
4990 }
4991 },
4992 _ => {
4993 return Err("Original object is not a segment".to_string());
4994 }
4995 };
4996
4997 let units = match &original_ctor {
4999 SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
5000 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
5001 _ => crate::pretty::NumericSuffix::Mm,
5002 },
5003 SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
5004 crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
5005 _ => crate::pretty::NumericSuffix::Mm,
5006 },
5007 _ => crate::pretty::NumericSuffix::Mm,
5008 };
5009
5010 let coords_to_point =
5013 |coords: Coords2d| -> crate::frontend::sketch::Point2d<crate::frontend::api::Number> {
5014 crate::frontend::sketch::Point2d {
5015 x: unit_to_number(coords.x, default_unit, units),
5016 y: unit_to_number(coords.y, default_unit, units),
5017 }
5018 };
5019
5020 let point_to_expr = |point: crate::frontend::sketch::Point2d<crate::frontend::api::Number>| -> crate::frontend::sketch::Point2d<crate::frontend::api::Expr> {
5022 crate::frontend::sketch::Point2d {
5023 x: crate::frontend::api::Expr::Var(point.x),
5024 y: crate::frontend::api::Expr::Var(point.y),
5025 }
5026 };
5027
5028 let new_segment_ctor = match &original_ctor {
5030 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
5031 start: point_to_expr(coords_to_point(*right_trim_coords)),
5032 end: point_to_expr(coords_to_point(*original_end_coords)),
5033 construction: line_ctor.construction,
5034 }),
5035 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
5036 start: point_to_expr(coords_to_point(*right_trim_coords)),
5037 end: point_to_expr(coords_to_point(*original_end_coords)),
5038 center: arc_ctor.center.clone(),
5039 construction: arc_ctor.construction,
5040 }),
5041 _ => {
5042 return Err("Unsupported segment type for new segment".to_string());
5043 }
5044 };
5045
5046 let (_add_source_delta, add_scene_graph_delta) = frontend
5047 .add_segment(ctx, version, sketch_id, new_segment_ctor, None)
5048 .await
5049 .map_err(|e| format!("Failed to add new segment: {}", e.error.message()))?;
5050
5051 let new_segment_id = *add_scene_graph_delta
5053 .new_objects
5054 .iter()
5055 .find(|&id| {
5056 if let Some(obj) = add_scene_graph_delta.new_graph.objects.iter().find(|o| o.id == *id) {
5057 matches!(
5058 &obj.kind,
5059 crate::frontend::api::ObjectKind::Segment { segment }
5060 if matches!(segment, crate::frontend::sketch::Segment::Line(_) | crate::frontend::sketch::Segment::Arc(_))
5061 )
5062 } else {
5063 false
5064 }
5065 })
5066 .ok_or_else(|| "Failed to find newly created segment".to_string())?;
5067
5068 let new_segment = add_scene_graph_delta
5069 .new_graph
5070 .objects
5071 .iter()
5072 .find(|o| o.id == new_segment_id)
5073 .ok_or_else(|| format!("New segment not found with id {}", new_segment_id.0))?;
5074
5075 let (new_segment_start_point_id, new_segment_end_point_id, new_segment_center_point_id) =
5077 match &new_segment.kind {
5078 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
5079 crate::frontend::sketch::Segment::Line(line) => (line.start, line.end, None),
5080 crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, Some(arc.center)),
5081 _ => {
5082 return Err("New segment is not a Line or Arc".to_string());
5083 }
5084 },
5085 _ => {
5086 return Err("New segment is not a segment".to_string());
5087 }
5088 };
5089
5090 let edited_ctor = match &original_ctor {
5092 SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
5093 start: line_ctor.start.clone(),
5094 end: point_to_expr(coords_to_point(*left_trim_coords)),
5095 construction: line_ctor.construction,
5096 }),
5097 SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
5098 start: arc_ctor.start.clone(),
5099 end: point_to_expr(coords_to_point(*left_trim_coords)),
5100 center: arc_ctor.center.clone(),
5101 construction: arc_ctor.construction,
5102 }),
5103 _ => {
5104 return Err("Unsupported segment type for split".to_string());
5105 }
5106 };
5107
5108 let (_edit_source_delta, edit_scene_graph_delta) = frontend
5109 .edit_segments(
5110 ctx,
5111 version,
5112 sketch_id,
5113 vec![ExistingSegmentCtor {
5114 id: *segment_id,
5115 ctor: edited_ctor,
5116 }],
5117 )
5118 .await
5119 .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))?;
5120 invalidates_ids = invalidates_ids || edit_scene_graph_delta.invalidates_ids;
5122
5123 let edited_segment = edit_scene_graph_delta
5125 .new_graph
5126 .objects
5127 .iter()
5128 .find(|obj| obj.id == *segment_id)
5129 .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
5130
5131 let left_side_endpoint_point_id = match &edited_segment.kind {
5132 crate::frontend::api::ObjectKind::Segment { segment } => match segment {
5133 crate::frontend::sketch::Segment::Line(line) => line.end,
5134 crate::frontend::sketch::Segment::Arc(arc) => arc.end,
5135 _ => {
5136 return Err("Edited segment is not a Line or Arc".to_string());
5137 }
5138 },
5139 _ => {
5140 return Err("Edited segment is not a segment".to_string());
5141 }
5142 };
5143
5144 let mut batch_constraints = Vec::new();
5146
5147 let left_intersecting_seg_id = match &**left_side {
5149 TrimTermination::Intersection {
5150 intersecting_seg_id, ..
5151 }
5152 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5153 intersecting_seg_id, ..
5154 } => *intersecting_seg_id,
5155 _ => {
5156 return Err("Left side is not an intersection or coincident".to_string());
5157 }
5158 };
5159 let left_coincident_segments = match &**left_side {
5160 TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5161 other_segment_point_id,
5162 ..
5163 } => {
5164 vec![left_side_endpoint_point_id.into(), (*other_segment_point_id).into()]
5165 }
5166 _ => {
5167 vec![left_side_endpoint_point_id.into(), left_intersecting_seg_id.into()]
5168 }
5169 };
5170 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
5171 segments: left_coincident_segments,
5172 }));
5173
5174 let right_intersecting_seg_id = match &**right_side {
5176 TrimTermination::Intersection {
5177 intersecting_seg_id, ..
5178 }
5179 | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5180 intersecting_seg_id, ..
5181 } => *intersecting_seg_id,
5182 _ => {
5183 return Err("Right side is not an intersection or coincident".to_string());
5184 }
5185 };
5186
5187 let mut intersection_point_id: Option<ObjectId> = None;
5188 if matches!(&**right_side, TrimTermination::Intersection { .. }) {
5189 let intersecting_seg = edit_scene_graph_delta
5190 .new_graph
5191 .objects
5192 .iter()
5193 .find(|obj| obj.id == right_intersecting_seg_id);
5194
5195 if let Some(seg) = intersecting_seg {
5196 let endpoint_epsilon = 1e-3; let right_trim_coords_value = *right_trim_coords;
5198
5199 if let crate::frontend::api::ObjectKind::Segment { segment } = &seg.kind {
5200 match segment {
5201 crate::frontend::sketch::Segment::Line(_) => {
5202 if let (Some(start_coords), Some(end_coords)) = (
5203 crate::frontend::trim::get_position_coords_for_line(
5204 seg,
5205 crate::frontend::trim::LineEndpoint::Start,
5206 &edit_scene_graph_delta.new_graph.objects,
5207 default_unit,
5208 ),
5209 crate::frontend::trim::get_position_coords_for_line(
5210 seg,
5211 crate::frontend::trim::LineEndpoint::End,
5212 &edit_scene_graph_delta.new_graph.objects,
5213 default_unit,
5214 ),
5215 ) {
5216 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
5217 * (right_trim_coords_value.x - start_coords.x)
5218 + (right_trim_coords_value.y - start_coords.y)
5219 * (right_trim_coords_value.y - start_coords.y))
5220 .sqrt();
5221 if dist_to_start < endpoint_epsilon {
5222 if let crate::frontend::sketch::Segment::Line(line) = segment {
5223 intersection_point_id = Some(line.start);
5224 }
5225 } else {
5226 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
5227 * (right_trim_coords_value.x - end_coords.x)
5228 + (right_trim_coords_value.y - end_coords.y)
5229 * (right_trim_coords_value.y - end_coords.y))
5230 .sqrt();
5231 if dist_to_end < endpoint_epsilon
5232 && let crate::frontend::sketch::Segment::Line(line) = segment
5233 {
5234 intersection_point_id = Some(line.end);
5235 }
5236 }
5237 }
5238 }
5239 crate::frontend::sketch::Segment::Arc(_) => {
5240 if let (Some(start_coords), Some(end_coords)) = (
5241 crate::frontend::trim::get_position_coords_from_arc(
5242 seg,
5243 crate::frontend::trim::ArcPoint::Start,
5244 &edit_scene_graph_delta.new_graph.objects,
5245 default_unit,
5246 ),
5247 crate::frontend::trim::get_position_coords_from_arc(
5248 seg,
5249 crate::frontend::trim::ArcPoint::End,
5250 &edit_scene_graph_delta.new_graph.objects,
5251 default_unit,
5252 ),
5253 ) {
5254 let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
5255 * (right_trim_coords_value.x - start_coords.x)
5256 + (right_trim_coords_value.y - start_coords.y)
5257 * (right_trim_coords_value.y - start_coords.y))
5258 .sqrt();
5259 if dist_to_start < endpoint_epsilon {
5260 if let crate::frontend::sketch::Segment::Arc(arc) = segment {
5261 intersection_point_id = Some(arc.start);
5262 }
5263 } else {
5264 let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
5265 * (right_trim_coords_value.x - end_coords.x)
5266 + (right_trim_coords_value.y - end_coords.y)
5267 * (right_trim_coords_value.y - end_coords.y))
5268 .sqrt();
5269 if dist_to_end < endpoint_epsilon
5270 && let crate::frontend::sketch::Segment::Arc(arc) = segment
5271 {
5272 intersection_point_id = Some(arc.end);
5273 }
5274 }
5275 }
5276 }
5277 _ => {}
5278 }
5279 }
5280 }
5281 }
5282
5283 let right_coincident_segments = if let Some(point_id) = intersection_point_id {
5284 vec![new_segment_start_point_id.into(), point_id.into()]
5285 } else if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5286 other_segment_point_id,
5287 ..
5288 } = &**right_side
5289 {
5290 vec![new_segment_start_point_id.into(), (*other_segment_point_id).into()]
5291 } else {
5292 vec![new_segment_start_point_id.into(), right_intersecting_seg_id.into()]
5293 };
5294 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
5295 segments: right_coincident_segments,
5296 }));
5297
5298 let mut points_constrained_to_new_segment_start = std::collections::HashSet::new();
5300 let mut points_constrained_to_new_segment_end = std::collections::HashSet::new();
5301
5302 if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5303 other_segment_point_id,
5304 ..
5305 } = &**right_side
5306 {
5307 points_constrained_to_new_segment_start.insert(other_segment_point_id);
5308 }
5309
5310 for constraint_to_migrate in constraints_to_migrate.iter() {
5311 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::End
5312 && constraint_to_migrate.is_point_point
5313 {
5314 points_constrained_to_new_segment_end.insert(constraint_to_migrate.other_entity_id);
5315 }
5316 }
5317
5318 for constraint_to_migrate in constraints_to_migrate.iter() {
5319 if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment
5321 && (points_constrained_to_new_segment_start.contains(&constraint_to_migrate.other_entity_id)
5322 || points_constrained_to_new_segment_end.contains(&constraint_to_migrate.other_entity_id))
5323 {
5324 continue; }
5326
5327 let constraint_segments = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment {
5328 vec![constraint_to_migrate.other_entity_id.into(), new_segment_id.into()]
5329 } else {
5330 let target_endpoint_id = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Start
5331 {
5332 new_segment_start_point_id
5333 } else {
5334 new_segment_end_point_id
5335 };
5336 vec![target_endpoint_id.into(), constraint_to_migrate.other_entity_id.into()]
5337 };
5338 batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
5339 segments: constraint_segments,
5340 }));
5341 }
5342
5343 let mut distance_constraints_to_re_add: Vec<(
5345 crate::frontend::api::Number,
5346 Option<crate::frontend::sketch::Point2d<crate::frontend::api::Number>>,
5347 crate::frontend::sketch::ConstraintSource,
5348 )> = Vec::new();
5349 if let (Some(original_start_id), Some(original_end_id)) =
5350 (original_segment_start_point_id, original_segment_end_point_id)
5351 {
5352 for obj in &edit_scene_graph_delta.new_graph.objects {
5353 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5354 continue;
5355 };
5356
5357 let Constraint::Distance(distance) = constraint else {
5358 continue;
5359 };
5360
5361 let references_start = distance.contains_point(original_start_id);
5362 let references_end = distance.contains_point(original_end_id);
5363
5364 if references_start && references_end {
5365 distance_constraints_to_re_add.push((
5366 distance.distance,
5367 distance.label_position.clone(),
5368 distance.source.clone(),
5369 ));
5370 }
5371 }
5372 }
5373
5374 if let Some(original_start_id) = original_segment_start_point_id {
5376 for (distance_value, label_position, source) in distance_constraints_to_re_add {
5377 batch_constraints.push(Constraint::Distance(crate::frontend::sketch::Distance {
5378 points: vec![original_start_id.into(), new_segment_end_point_id.into()],
5379 distance: distance_value,
5380 label_position,
5381 source,
5382 }));
5383 }
5384 }
5385
5386 if let Some(new_center_id) = new_segment_center_point_id {
5388 for (constraint, original_center_id) in center_point_constraints_to_migrate {
5389 let center_rewrite_map = std::collections::HashMap::from([(original_center_id, new_center_id)]);
5390 if let Some(rewritten) = rewrite_constraint_with_map(&constraint, ¢er_rewrite_map)
5391 && matches!(rewritten, Constraint::Coincident(_) | Constraint::Distance(_))
5392 {
5393 batch_constraints.push(rewritten);
5394 }
5395 }
5396 }
5397
5398 let mut angle_rewrite_map = std::collections::HashMap::from([(*segment_id, new_segment_id)]);
5400 if let Some(original_end_id) = original_segment_end_point_id {
5401 angle_rewrite_map.insert(original_end_id, new_segment_end_point_id);
5402 }
5403 for obj in &edit_scene_graph_delta.new_graph.objects {
5404 let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5405 continue;
5406 };
5407
5408 let should_migrate = match constraint {
5411 Constraint::Parallel(parallel) => parallel.lines.contains(segment_id),
5412 Constraint::Perpendicular(perpendicular) => perpendicular.lines.contains(segment_id),
5413 Constraint::Horizontal(Horizontal::Line { line }) => line == segment_id,
5414 Constraint::Horizontal(Horizontal::Points { points }) => original_segment_end_point_id
5415 .is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
5416 Constraint::Vertical(Vertical::Line { line }) => line == segment_id,
5417 Constraint::Vertical(Vertical::Points { points }) => original_segment_end_point_id
5418 .is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
5419 Constraint::Angle(_)
5420 | Constraint::Coincident(_)
5421 | Constraint::Diameter(_)
5422 | Constraint::Distance(_)
5423 | Constraint::EqualRadius(_)
5424 | Constraint::Fixed(_)
5425 | Constraint::HorizontalDistance(_)
5426 | Constraint::LinesEqualLength(_)
5427 | Constraint::Midpoint(_)
5428 | Constraint::Radius(_)
5429 | Constraint::Symmetric(_)
5430 | Constraint::Tangent(_)
5431 | Constraint::VerticalDistance(_) => false,
5432 };
5433
5434 if should_migrate
5435 && let Some(migrated_constraint) = rewrite_constraint_with_map(constraint, &angle_rewrite_map)
5436 && matches!(
5437 migrated_constraint,
5438 Constraint::Parallel(_)
5439 | Constraint::Perpendicular(_)
5440 | Constraint::Horizontal(_)
5441 | Constraint::Vertical(_)
5442 )
5443 {
5444 batch_constraints.push(migrated_constraint);
5445 }
5446 }
5447
5448 let constraint_object_ids: Vec<ObjectId> = constraints_to_delete.to_vec();
5450
5451 let batch_result = frontend
5452 .batch_split_segment_operations(
5453 ctx,
5454 version,
5455 sketch_id,
5456 Vec::new(), batch_constraints,
5458 constraint_object_ids,
5459 crate::frontend::sketch::NewSegmentInfo {
5460 segment_id: new_segment_id,
5461 start_point_id: new_segment_start_point_id,
5462 end_point_id: new_segment_end_point_id,
5463 center_point_id: new_segment_center_point_id,
5464 },
5465 )
5466 .await
5467 .map_err(|e| format!("Failed to batch split segment operations: {}", e.error.message()));
5468 if let Ok((_, ref batch_delta)) = batch_result {
5470 invalidates_ids = invalidates_ids || batch_delta.invalidates_ids;
5471 }
5472 batch_result
5473 }
5474 };
5475
5476 match operation_result {
5477 Ok((source_delta, scene_graph_delta)) => {
5478 invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
5480 last_result = Some((source_delta, scene_graph_delta.clone()));
5481 }
5482 Err(e) => {
5483 crate::logln!("Error executing trim operation {}: {}", op_index, e);
5484 }
5486 }
5487
5488 op_index += consumed_ops;
5489 }
5490
5491 let (source_delta, mut scene_graph_delta) =
5492 last_result.ok_or_else(|| "No operations were executed successfully".to_string())?;
5493 scene_graph_delta.invalidates_ids = invalidates_ids;
5495 Ok((source_delta, scene_graph_delta))
5496}