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