Skip to main content

kcl_lib/frontend/
trim.rs

1use std::f64::consts::TAU;
2
3use indexmap::IndexSet;
4use kittycad_modeling_cmds::units::UnitLength;
5
6use crate::execution::types::adjust_length;
7use crate::front::Horizontal;
8use crate::front::Vertical;
9use crate::frontend::api::Number;
10use crate::frontend::api::Object;
11use crate::frontend::api::ObjectId;
12use crate::frontend::api::ObjectKind;
13use crate::frontend::sketch::Constraint;
14use crate::frontend::sketch::ConstraintSegment;
15use crate::frontend::sketch::Segment;
16use crate::frontend::sketch::SegmentCtor;
17use crate::pretty::NumericSuffix;
18
19#[cfg(all(feature = "artifact-graph", test))]
20mod tests;
21
22// Epsilon constants for geometric calculations
23const EPSILON_PARALLEL: f64 = 1e-10;
24const EPSILON_POINT_ON_SEGMENT: f64 = 1e-6;
25const EPSILON_COINCIDENT_TERMINATION_SNAP: f64 = 5e-2;
26
27/// Length unit for a numeric suffix (length variants only). Non-length suffixes default to millimeters.
28fn suffix_to_unit(suffix: NumericSuffix) -> UnitLength {
29    match suffix {
30        NumericSuffix::Mm => UnitLength::Millimeters,
31        NumericSuffix::Cm => UnitLength::Centimeters,
32        NumericSuffix::M => UnitLength::Meters,
33        NumericSuffix::Inch => UnitLength::Inches,
34        NumericSuffix::Ft => UnitLength::Feet,
35        NumericSuffix::Yd => UnitLength::Yards,
36        _ => UnitLength::Millimeters,
37    }
38}
39
40/// Convert a length `Number` to f64 in the target unit. Use when normalizing geometry into a single unit.
41fn number_to_unit(n: &Number, target_unit: UnitLength) -> f64 {
42    adjust_length(suffix_to_unit(n.units), n.value, target_unit).0
43}
44
45/// Convert a length in the given unit to a `Number` in the target suffix.
46fn unit_to_number(value: f64, source_unit: UnitLength, target_suffix: NumericSuffix) -> Number {
47    let (value, _) = adjust_length(source_unit, value, suffix_to_unit(target_suffix));
48    Number {
49        value,
50        units: target_suffix,
51    }
52}
53
54/// Convert trim line points from millimeters into the current/default unit.
55fn normalize_trim_points_to_unit(points: &[Coords2d], default_unit: UnitLength) -> Vec<Coords2d> {
56    points
57        .iter()
58        .map(|point| Coords2d {
59            x: adjust_length(UnitLength::Millimeters, point.x, default_unit).0,
60            y: adjust_length(UnitLength::Millimeters, point.y, default_unit).0,
61        })
62        .collect()
63}
64
65/// 2D coordinates in the trim internal unit (current/default length unit).
66#[derive(Debug, Clone, Copy)]
67pub struct Coords2d {
68    pub x: f64,
69    pub y: f64,
70}
71
72/// Which endpoint of a line segment to get coordinates for
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum LineEndpoint {
75    Start,
76    End,
77}
78
79/// Which point of an arc segment to get coordinates for
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum ArcPoint {
82    Start,
83    End,
84    Center,
85}
86
87/// Which point of a circle segment to get coordinates for
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum CirclePoint {
90    Start,
91    Center,
92}
93
94/// Direction along a segment for finding trim terminations
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum TrimDirection {
97    Left,
98    Right,
99}
100
101// Manual serde implementation for Coords2d to serialize as [x, y] array
102// This matches TypeScript's Coords2d type which is [number, number]
103
104// A trim spawn is the intersection point of the trim line (drawn by the user) and a segment.
105// We travel in both directions along the segment from the trim spawn to determine how to implement the trim.
106
107/// Item from advancing to the next trim spawn (intersection), like an iterator item from `Iterator::next()`.
108#[derive(Debug, Clone)]
109pub enum TrimItem {
110    Spawn {
111        trim_spawn_seg_id: ObjectId,
112        trim_spawn_coords: Coords2d,
113        next_index: usize,
114    },
115    None {
116        next_index: usize,
117    },
118}
119
120/// Trim termination types
121///
122/// Trim termination is the term used to figure out each end of a segment after a trim spawn has been found.
123/// When a trim spawn is found, we travel in both directions to find this termination. It can be:
124/// (1) the end of a segment (floating end), (2) an intersection with another segment, or
125/// (3) a coincident point where another segment is coincident with the segment we're traveling along.
126#[derive(Debug, Clone)]
127pub enum TrimTermination {
128    SegEndPoint {
129        trim_termination_coords: Coords2d,
130    },
131    Intersection {
132        trim_termination_coords: Coords2d,
133        intersecting_seg_id: ObjectId,
134    },
135    TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
136        trim_termination_coords: Coords2d,
137        intersecting_seg_id: ObjectId,
138        other_segment_point_id: ObjectId,
139    },
140}
141
142/// Trim terminations for both sides
143#[derive(Debug, Clone)]
144pub struct TrimTerminations {
145    pub left_side: TrimTermination,
146    pub right_side: TrimTermination,
147}
148
149/// Specifies where a constraint should attach when migrating during split operations
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum AttachToEndpoint {
152    Start,
153    End,
154    Segment,
155}
156
157/// Specifies which endpoint of a segment was changed
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159pub enum EndpointChanged {
160    Start,
161    End,
162}
163
164/// Coincident data for split segment operations
165#[derive(Debug, Clone)]
166pub struct CoincidentData {
167    pub intersecting_seg_id: ObjectId,
168    pub intersecting_endpoint_point_id: Option<ObjectId>,
169    pub existing_point_segment_constraint_id: Option<ObjectId>,
170}
171
172/// Constraint to migrate during split operations
173#[derive(Debug, Clone)]
174pub struct ConstraintToMigrate {
175    pub constraint_id: ObjectId,
176    pub other_entity_id: ObjectId,
177    /// True if the coincident constraint is between two points (point–point).
178    /// False if it is between a point and a line/arc/segment (point-segment coincident).
179    pub is_point_point: bool,
180    pub attach_to_endpoint: AttachToEndpoint,
181}
182
183/// Semantic trim plan produced by analysis/planning before lowering to frontend operations.
184#[derive(Debug, Clone)]
185pub enum TrimPlan {
186    DeleteSegment {
187        segment_id: ObjectId,
188    },
189    TailCut {
190        segment_id: ObjectId,
191        endpoint_changed: EndpointChanged,
192        ctor: SegmentCtor,
193        segment_or_point_to_make_coincident_to: ObjectId,
194        intersecting_endpoint_point_id: Option<ObjectId>,
195        constraint_ids_to_delete: Vec<ObjectId>,
196        additional_edited_segment_ids: Vec<ObjectId>,
197    },
198    ReplaceCircleWithArc {
199        circle_id: ObjectId,
200        arc_start_coords: Coords2d,
201        arc_end_coords: Coords2d,
202        arc_start_termination: Box<TrimTermination>,
203        arc_end_termination: Box<TrimTermination>,
204    },
205    SplitSegment {
206        segment_id: ObjectId,
207        left_trim_coords: Coords2d,
208        right_trim_coords: Coords2d,
209        original_end_coords: Coords2d,
210        left_side: Box<TrimTermination>,
211        right_side: Box<TrimTermination>,
212        left_side_coincident_data: CoincidentData,
213        right_side_coincident_data: CoincidentData,
214        constraints_to_migrate: Vec<ConstraintToMigrate>,
215        constraints_to_delete: Vec<ObjectId>,
216    },
217}
218
219fn lower_trim_plan(plan: &TrimPlan) -> Vec<TrimOperation> {
220    match plan {
221        TrimPlan::DeleteSegment { segment_id } => vec![TrimOperation::SimpleTrim {
222            segment_to_trim_id: *segment_id,
223        }],
224        TrimPlan::TailCut {
225            segment_id,
226            endpoint_changed,
227            ctor,
228            segment_or_point_to_make_coincident_to,
229            intersecting_endpoint_point_id,
230            constraint_ids_to_delete,
231            additional_edited_segment_ids,
232        } => {
233            let mut ops = vec![
234                TrimOperation::EditSegment {
235                    segment_id: *segment_id,
236                    ctor: ctor.clone(),
237                    endpoint_changed: *endpoint_changed,
238                    additional_edited_segment_ids: additional_edited_segment_ids.clone(),
239                },
240                TrimOperation::AddCoincidentConstraint {
241                    segment_id: *segment_id,
242                    endpoint_changed: *endpoint_changed,
243                    segment_or_point_to_make_coincident_to: *segment_or_point_to_make_coincident_to,
244                    intersecting_endpoint_point_id: *intersecting_endpoint_point_id,
245                },
246            ];
247            if !constraint_ids_to_delete.is_empty() {
248                ops.push(TrimOperation::DeleteConstraints {
249                    constraint_ids: constraint_ids_to_delete.clone(),
250                });
251            }
252            ops
253        }
254        TrimPlan::ReplaceCircleWithArc {
255            circle_id,
256            arc_start_coords,
257            arc_end_coords,
258            arc_start_termination,
259            arc_end_termination,
260        } => vec![TrimOperation::ReplaceCircleWithArc {
261            circle_id: *circle_id,
262            arc_start_coords: *arc_start_coords,
263            arc_end_coords: *arc_end_coords,
264            arc_start_termination: arc_start_termination.clone(),
265            arc_end_termination: arc_end_termination.clone(),
266        }],
267        TrimPlan::SplitSegment {
268            segment_id,
269            left_trim_coords,
270            right_trim_coords,
271            original_end_coords,
272            left_side,
273            right_side,
274            left_side_coincident_data,
275            right_side_coincident_data,
276            constraints_to_migrate,
277            constraints_to_delete,
278        } => vec![TrimOperation::SplitSegment {
279            segment_id: *segment_id,
280            left_trim_coords: *left_trim_coords,
281            right_trim_coords: *right_trim_coords,
282            original_end_coords: *original_end_coords,
283            left_side: left_side.clone(),
284            right_side: right_side.clone(),
285            left_side_coincident_data: left_side_coincident_data.clone(),
286            right_side_coincident_data: right_side_coincident_data.clone(),
287            constraints_to_migrate: constraints_to_migrate.clone(),
288            constraints_to_delete: constraints_to_delete.clone(),
289        }],
290    }
291}
292
293fn trim_plan_modifies_geometry(plan: &TrimPlan) -> bool {
294    matches!(
295        plan,
296        TrimPlan::DeleteSegment { .. }
297            | TrimPlan::TailCut { .. }
298            | TrimPlan::ReplaceCircleWithArc { .. }
299            | TrimPlan::SplitSegment { .. }
300    )
301}
302
303fn rewrite_object_id(id: ObjectId, rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>) -> ObjectId {
304    rewrite_map.get(&id).copied().unwrap_or(id)
305}
306
307fn rewrite_constraint_segment(
308    segment: crate::frontend::sketch::ConstraintSegment,
309    rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
310) -> crate::frontend::sketch::ConstraintSegment {
311    match segment {
312        crate::frontend::sketch::ConstraintSegment::Segment(id) => {
313            crate::frontend::sketch::ConstraintSegment::Segment(rewrite_object_id(id, rewrite_map))
314        }
315        crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
316            crate::frontend::sketch::ConstraintSegment::Origin(origin)
317        }
318    }
319}
320
321fn rewrite_constraint_segments(
322    segments: &[crate::frontend::sketch::ConstraintSegment],
323    rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
324) -> Vec<crate::frontend::sketch::ConstraintSegment> {
325    segments
326        .iter()
327        .copied()
328        .map(|segment| rewrite_constraint_segment(segment, rewrite_map))
329        .collect()
330}
331
332fn constraint_segments_reference_any(
333    segments: &[crate::frontend::sketch::ConstraintSegment],
334    ids: &std::collections::HashSet<ObjectId>,
335) -> bool {
336    segments.iter().any(|segment| match segment {
337        crate::frontend::sketch::ConstraintSegment::Segment(id) => ids.contains(id),
338        crate::frontend::sketch::ConstraintSegment::Origin(_) => false,
339    })
340}
341
342fn rewrite_constraint_with_map(
343    constraint: &Constraint,
344    rewrite_map: &std::collections::HashMap<ObjectId, ObjectId>,
345) -> Option<Constraint> {
346    // Keep trim constraint matches exhaustive. New constraints can break trim in
347    // unexpected ways; try trimming sketches that use the new constraint and ask
348    // Kurt, Max, or a mechanical engineer when the expected behavior is unclear.
349    match constraint {
350        Constraint::Coincident(coincident) => Some(Constraint::Coincident(crate::frontend::sketch::Coincident {
351            segments: rewrite_constraint_segments(&coincident.segments, rewrite_map),
352        })),
353        Constraint::Distance(distance) => Some(Constraint::Distance(crate::frontend::sketch::Distance {
354            points: rewrite_constraint_segments(&distance.points, rewrite_map),
355            distance: distance.distance,
356            label_position: distance.label_position.clone(),
357            source: distance.source.clone(),
358        })),
359        Constraint::HorizontalDistance(distance) => {
360            Some(Constraint::HorizontalDistance(crate::frontend::sketch::Distance {
361                points: rewrite_constraint_segments(&distance.points, rewrite_map),
362                distance: distance.distance,
363                label_position: distance.label_position.clone(),
364                source: distance.source.clone(),
365            }))
366        }
367        Constraint::VerticalDistance(distance) => {
368            Some(Constraint::VerticalDistance(crate::frontend::sketch::Distance {
369                points: rewrite_constraint_segments(&distance.points, rewrite_map),
370                distance: distance.distance,
371                label_position: distance.label_position.clone(),
372                source: distance.source.clone(),
373            }))
374        }
375        Constraint::Radius(radius) => Some(Constraint::Radius(crate::frontend::sketch::Radius {
376            arc: rewrite_object_id(radius.arc, rewrite_map),
377            radius: radius.radius,
378            label_position: radius.label_position.clone(),
379            source: radius.source.clone(),
380        })),
381        Constraint::Diameter(diameter) => Some(Constraint::Diameter(crate::frontend::sketch::Diameter {
382            arc: rewrite_object_id(diameter.arc, rewrite_map),
383            diameter: diameter.diameter,
384            label_position: diameter.label_position.clone(),
385            source: diameter.source.clone(),
386        })),
387        Constraint::EqualRadius(equal_radius) => Some(Constraint::EqualRadius(crate::frontend::sketch::EqualRadius {
388            input: equal_radius
389                .input
390                .iter()
391                .map(|id| rewrite_object_id(*id, rewrite_map))
392                .collect(),
393        })),
394        Constraint::Midpoint(midpoint) => Some(Constraint::Midpoint(crate::frontend::sketch::Midpoint {
395            point: rewrite_object_id(midpoint.point, rewrite_map),
396            segment: rewrite_object_id(midpoint.segment, rewrite_map),
397        })),
398        Constraint::Tangent(tangent) => Some(Constraint::Tangent(crate::frontend::sketch::Tangent {
399            input: tangent
400                .input
401                .iter()
402                .map(|id| rewrite_object_id(*id, rewrite_map))
403                .collect(),
404        })),
405        Constraint::Symmetric(symmetric) => Some(Constraint::Symmetric(crate::frontend::sketch::Symmetric {
406            input: symmetric
407                .input
408                .iter()
409                .map(|id| rewrite_object_id(*id, rewrite_map))
410                .collect(),
411            axis: rewrite_object_id(symmetric.axis, rewrite_map),
412        })),
413        Constraint::Parallel(parallel) => Some(Constraint::Parallel(crate::frontend::sketch::Parallel {
414            lines: parallel
415                .lines
416                .iter()
417                .map(|id| rewrite_object_id(*id, rewrite_map))
418                .collect(),
419        })),
420        Constraint::Perpendicular(perpendicular) => {
421            Some(Constraint::Perpendicular(crate::frontend::sketch::Perpendicular {
422                lines: perpendicular
423                    .lines
424                    .iter()
425                    .map(|id| rewrite_object_id(*id, rewrite_map))
426                    .collect(),
427            }))
428        }
429        Constraint::Horizontal(horizontal) => match horizontal {
430            crate::front::Horizontal::Line { line } => {
431                Some(Constraint::Horizontal(crate::frontend::sketch::Horizontal::Line {
432                    line: rewrite_object_id(*line, rewrite_map),
433                }))
434            }
435            crate::front::Horizontal::Points { points } => Some(Constraint::Horizontal(Horizontal::Points {
436                points: points
437                    .iter()
438                    .map(|point| match point {
439                        crate::frontend::sketch::ConstraintSegment::Segment(point) => {
440                            crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
441                        }
442                        crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
443                            crate::frontend::sketch::ConstraintSegment::Origin(*origin)
444                        }
445                    })
446                    .collect(),
447            })),
448        },
449        Constraint::Vertical(vertical) => match vertical {
450            crate::front::Vertical::Line { line } => {
451                Some(Constraint::Vertical(crate::frontend::sketch::Vertical::Line {
452                    line: rewrite_object_id(*line, rewrite_map),
453                }))
454            }
455            crate::front::Vertical::Points { points } => Some(Constraint::Vertical(Vertical::Points {
456                points: points
457                    .iter()
458                    .map(|point| match point {
459                        crate::frontend::sketch::ConstraintSegment::Segment(point) => {
460                            crate::frontend::sketch::ConstraintSegment::from(rewrite_object_id(*point, rewrite_map))
461                        }
462                        crate::frontend::sketch::ConstraintSegment::Origin(origin) => {
463                            crate::frontend::sketch::ConstraintSegment::Origin(*origin)
464                        }
465                    })
466                    .collect(),
467            })),
468        },
469        Constraint::Angle(_) | Constraint::Fixed(_) | Constraint::LinesEqualLength(_) => None,
470    }
471}
472
473fn point_axis_constraint_references_point(constraint: &Constraint, point_id: ObjectId) -> bool {
474    // Keep trim constraint matches exhaustive. New constraints should make an
475    // explicit preserve/delete/migrate decision rather than falling through.
476    match constraint {
477        Constraint::Horizontal(Horizontal::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
478        Constraint::Vertical(Vertical::Points { points }) => points.contains(&ConstraintSegment::from(point_id)),
479        Constraint::Angle(_)
480        | Constraint::Coincident(_)
481        | Constraint::Diameter(_)
482        | Constraint::Distance(_)
483        | Constraint::EqualRadius(_)
484        | Constraint::Fixed(_)
485        | Constraint::Horizontal(Horizontal::Line { .. })
486        | Constraint::HorizontalDistance(_)
487        | Constraint::LinesEqualLength(_)
488        | Constraint::Midpoint(_)
489        | Constraint::Parallel(_)
490        | Constraint::Perpendicular(_)
491        | Constraint::Radius(_)
492        | Constraint::Symmetric(_)
493        | Constraint::Tangent(_)
494        | Constraint::Vertical(Vertical::Line { .. })
495        | Constraint::VerticalDistance(_) => false,
496    }
497}
498
499fn owner_or_segment_id(objects: &[Object], segment_id: ObjectId) -> ObjectId {
500    if let Some(segment_object) = objects.iter().find(|obj| obj.id == segment_id)
501        && let ObjectKind::Segment {
502            segment: Segment::Point(point),
503        } = &segment_object.kind
504        && let Some(owner_id) = point.owner
505    {
506        owner_id
507    } else {
508        segment_id
509    }
510}
511
512fn segment_id_is_or_is_owned_by_curve(objects: &[Object], segment_id: ObjectId) -> bool {
513    objects.iter().find(|obj| obj.id == segment_id).is_some_and(|object| {
514        let ObjectKind::Segment { segment } = &object.kind else {
515            return false;
516        };
517
518        match segment {
519            Segment::Arc(_) | Segment::Circle(_) => true,
520            Segment::Point(point) => point.owner.is_some_and(|owner_id| {
521                objects.iter().find(|obj| obj.id == owner_id).is_some_and(|owner| {
522                    matches!(
523                        owner.kind,
524                        ObjectKind::Segment {
525                            segment: Segment::Arc(_) | Segment::Circle(_)
526                        }
527                    )
528                })
529            }),
530            _ => false,
531        }
532    })
533}
534
535fn sketch_segment_ids_for_segment(objects: &[Object], segment_id: ObjectId) -> Vec<ObjectId> {
536    objects
537        .iter()
538        .find_map(|obj| {
539            let ObjectKind::Sketch(sketch) = &obj.kind else {
540                return None;
541            };
542
543            sketch.segments.contains(&segment_id).then(|| sketch.segments.clone())
544        })
545        .unwrap_or_default()
546}
547
548#[derive(Debug, Clone)]
549#[allow(clippy::large_enum_variant)]
550pub enum TrimOperation {
551    SimpleTrim {
552        segment_to_trim_id: ObjectId,
553    },
554    EditSegment {
555        segment_id: ObjectId,
556        ctor: SegmentCtor,
557        endpoint_changed: EndpointChanged,
558        additional_edited_segment_ids: Vec<ObjectId>,
559    },
560    AddCoincidentConstraint {
561        segment_id: ObjectId,
562        endpoint_changed: EndpointChanged,
563        segment_or_point_to_make_coincident_to: ObjectId,
564        intersecting_endpoint_point_id: Option<ObjectId>,
565    },
566    SplitSegment {
567        segment_id: ObjectId,
568        left_trim_coords: Coords2d,
569        right_trim_coords: Coords2d,
570        original_end_coords: Coords2d,
571        left_side: Box<TrimTermination>,
572        right_side: Box<TrimTermination>,
573        left_side_coincident_data: CoincidentData,
574        right_side_coincident_data: CoincidentData,
575        constraints_to_migrate: Vec<ConstraintToMigrate>,
576        constraints_to_delete: Vec<ObjectId>,
577    },
578    ReplaceCircleWithArc {
579        circle_id: ObjectId,
580        arc_start_coords: Coords2d,
581        arc_end_coords: Coords2d,
582        arc_start_termination: Box<TrimTermination>,
583        arc_end_termination: Box<TrimTermination>,
584    },
585    DeleteConstraints {
586        constraint_ids: Vec<ObjectId>,
587    },
588}
589
590/// Helper to check if a point is on a line segment (within epsilon distance)
591///
592/// Returns the point if it's on the segment, None otherwise.
593pub fn is_point_on_line_segment(
594    point: Coords2d,
595    segment_start: Coords2d,
596    segment_end: Coords2d,
597    epsilon: f64,
598) -> Option<Coords2d> {
599    let dx = segment_end.x - segment_start.x;
600    let dy = segment_end.y - segment_start.y;
601    let segment_length_sq = dx * dx + dy * dy;
602
603    if segment_length_sq < EPSILON_PARALLEL {
604        // Segment is degenerate, i.e it's practically a point
605        let dist_sq = (point.x - segment_start.x) * (point.x - segment_start.x)
606            + (point.y - segment_start.y) * (point.y - segment_start.y);
607        if dist_sq <= epsilon * epsilon {
608            return Some(point);
609        }
610        return None;
611    }
612
613    let point_dx = point.x - segment_start.x;
614    let point_dy = point.y - segment_start.y;
615    let projection_param = (point_dx * dx + point_dy * dy) / segment_length_sq;
616
617    // Check if point projects onto the segment
618    if !(0.0..=1.0).contains(&projection_param) {
619        return None;
620    }
621
622    // Calculate the projected point on the segment
623    let projected_point = Coords2d {
624        x: segment_start.x + projection_param * dx,
625        y: segment_start.y + projection_param * dy,
626    };
627
628    // Check if the distance from point to projected point is within epsilon
629    let dist_dx = point.x - projected_point.x;
630    let dist_dy = point.y - projected_point.y;
631    let distance_sq = dist_dx * dist_dx + dist_dy * dist_dy;
632
633    if distance_sq <= epsilon * epsilon {
634        Some(point)
635    } else {
636        None
637    }
638}
639
640/// Helper to calculate intersection point of two line segments
641///
642/// Returns the intersection point if segments intersect, None otherwise.
643pub fn line_segment_intersection(
644    line1_start: Coords2d,
645    line1_end: Coords2d,
646    line2_start: Coords2d,
647    line2_end: Coords2d,
648    epsilon: f64,
649) -> Option<Coords2d> {
650    // First check if any endpoints are on the other segment
651    if let Some(point) = is_point_on_line_segment(line1_start, line2_start, line2_end, epsilon) {
652        return Some(point);
653    }
654
655    if let Some(point) = is_point_on_line_segment(line1_end, line2_start, line2_end, epsilon) {
656        return Some(point);
657    }
658
659    if let Some(point) = is_point_on_line_segment(line2_start, line1_start, line1_end, epsilon) {
660        return Some(point);
661    }
662
663    if let Some(point) = is_point_on_line_segment(line2_end, line1_start, line1_end, epsilon) {
664        return Some(point);
665    }
666
667    // Then check for actual line segment intersection
668    let x1 = line1_start.x;
669    let y1 = line1_start.y;
670    let x2 = line1_end.x;
671    let y2 = line1_end.y;
672    let x3 = line2_start.x;
673    let y3 = line2_start.y;
674    let x4 = line2_end.x;
675    let y4 = line2_end.y;
676
677    let denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
678    if denominator.abs() < EPSILON_PARALLEL {
679        // Lines are parallel
680        return None;
681    }
682
683    let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denominator;
684    let u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denominator;
685
686    // Check if intersection is within both segments
687    if (0.0..=1.0).contains(&t) && (0.0..=1.0).contains(&u) {
688        let x = x1 + t * (x2 - x1);
689        let y = y1 + t * (y2 - y1);
690        return Some(Coords2d { x, y });
691    }
692
693    None
694}
695
696/// Helper to calculate the parametric position of a point on a line segment
697///
698/// Returns t where t=0 at segmentStart, t=1 at segmentEnd.
699/// t can be < 0 or > 1 if the point projects outside the segment.
700pub fn project_point_onto_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
701    let dx = segment_end.x - segment_start.x;
702    let dy = segment_end.y - segment_start.y;
703    let segment_length_sq = dx * dx + dy * dy;
704
705    if segment_length_sq < EPSILON_PARALLEL {
706        // Segment is degenerate
707        return 0.0;
708    }
709
710    let point_dx = point.x - segment_start.x;
711    let point_dy = point.y - segment_start.y;
712
713    (point_dx * dx + point_dy * dy) / segment_length_sq
714}
715
716/// Helper to calculate the perpendicular distance from a point to a line segment
717///
718/// Returns the distance from the point to the closest point on the segment.
719pub fn perpendicular_distance_to_segment(point: Coords2d, segment_start: Coords2d, segment_end: Coords2d) -> f64 {
720    let dx = segment_end.x - segment_start.x;
721    let dy = segment_end.y - segment_start.y;
722    let segment_length_sq = dx * dx + dy * dy;
723
724    if segment_length_sq < EPSILON_PARALLEL {
725        // Segment is degenerate, return distance to point
726        let dist_dx = point.x - segment_start.x;
727        let dist_dy = point.y - segment_start.y;
728        return (dist_dx * dist_dx + dist_dy * dist_dy).sqrt();
729    }
730
731    // Vector from segment start to point
732    let point_dx = point.x - segment_start.x;
733    let point_dy = point.y - segment_start.y;
734
735    // Project point onto segment
736    let t = (point_dx * dx + point_dy * dy) / segment_length_sq;
737
738    // Clamp t to [0, 1] to get closest point on segment
739    let clamped_t = t.clamp(0.0, 1.0);
740    let closest_point = Coords2d {
741        x: segment_start.x + clamped_t * dx,
742        y: segment_start.y + clamped_t * dy,
743    };
744
745    // Calculate distance
746    let dist_dx = point.x - closest_point.x;
747    let dist_dy = point.y - closest_point.y;
748    (dist_dx * dist_dx + dist_dy * dist_dy).sqrt()
749}
750
751/// Helper to check if a point is on an arc segment (CCW from start to end)
752///
753/// Returns true if the point is on the arc, false otherwise.
754pub fn is_point_on_arc(point: Coords2d, center: Coords2d, start: Coords2d, end: Coords2d, epsilon: f64) -> bool {
755    // Calculate radius
756    let radius = ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
757
758    // Check if point is on the circle (within epsilon)
759    let dist_from_center =
760        ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
761    if (dist_from_center - radius).abs() > epsilon {
762        return false;
763    }
764
765    // Calculate angles
766    let start_angle = libm::atan2(start.y - center.y, start.x - center.x);
767    let end_angle = libm::atan2(end.y - center.y, end.x - center.x);
768    let point_angle = libm::atan2(point.y - center.y, point.x - center.x);
769
770    // Normalize angles to [0, 2Ï€]
771    let normalize_angle = |angle: f64| -> f64 {
772        if !angle.is_finite() {
773            return angle;
774        }
775        let mut normalized = angle;
776        while normalized < 0.0 {
777            normalized += TAU;
778        }
779        while normalized >= TAU {
780            normalized -= TAU;
781        }
782        normalized
783    };
784
785    let normalized_start = normalize_angle(start_angle);
786    let normalized_end = normalize_angle(end_angle);
787    let normalized_point = normalize_angle(point_angle);
788
789    // Check if point is on the arc going CCW from start to end
790    // Since arcs always travel CCW, we need to check if the point angle
791    // is between start and end when going CCW
792    if normalized_start < normalized_end {
793        // No wrap around
794        normalized_point >= normalized_start && normalized_point <= normalized_end
795    } else {
796        // Wrap around (e.g., start at 350°, end at 10°)
797        normalized_point >= normalized_start || normalized_point <= normalized_end
798    }
799}
800
801/// Helper to calculate intersections between a line segment and an arc.
802///
803/// Returns intersections sorted by the line segment parametric position.
804pub fn line_arc_intersections(
805    line_start: Coords2d,
806    line_end: Coords2d,
807    arc_center: Coords2d,
808    arc_start: Coords2d,
809    arc_end: Coords2d,
810    epsilon: f64,
811) -> Vec<(f64, Coords2d)> {
812    // Calculate radius
813    let radius = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
814        + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
815        .sqrt();
816
817    // Translate line to origin (center at 0,0)
818    let translated_line_start = Coords2d {
819        x: line_start.x - arc_center.x,
820        y: line_start.y - arc_center.y,
821    };
822    let translated_line_end = Coords2d {
823        x: line_end.x - arc_center.x,
824        y: line_end.y - arc_center.y,
825    };
826
827    // Line equation: p = lineStart + t * (lineEnd - lineStart)
828    let dx = translated_line_end.x - translated_line_start.x;
829    let dy = translated_line_end.y - translated_line_start.y;
830
831    // Circle equation: x² + y² = r²
832    // Substitute line equation into circle equation
833    // (x0 + t*dx)² + (y0 + t*dy)² = r²
834    // Expand: x0² + 2*x0*t*dx + t²*dx² + y0² + 2*y0*t*dy + t²*dy² = r²
835    // Rearrange: t²*(dx² + dy²) + 2*t*(x0*dx + y0*dy) + (x0² + y0² - r²) = 0
836
837    let a = dx * dx + dy * dy;
838    let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
839    let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
840        - radius * radius;
841
842    let discriminant = b * b - 4.0 * a * c;
843
844    if discriminant < 0.0 {
845        // No intersection
846        return Vec::new();
847    }
848
849    if a.abs() < EPSILON_PARALLEL {
850        // Line segment is degenerate
851        let dist_from_center = (translated_line_start.x * translated_line_start.x
852            + translated_line_start.y * translated_line_start.y)
853            .sqrt();
854        if (dist_from_center - radius).abs() <= epsilon {
855            // Point is on circle, check if it's on the arc
856            let point = line_start;
857            if is_point_on_arc(point, arc_center, arc_start, arc_end, epsilon) {
858                return vec![(0.0, point)];
859            }
860        }
861        return Vec::new();
862    }
863
864    let sqrt_discriminant = discriminant.sqrt();
865    let t1 = (-b - sqrt_discriminant) / (2.0 * a);
866    let t2 = (-b + sqrt_discriminant) / (2.0 * a);
867
868    // Check both intersection points
869    let mut candidates: Vec<(f64, Coords2d)> = Vec::new();
870    if (0.0..=1.0).contains(&t1) {
871        let point = Coords2d {
872            x: line_start.x + t1 * (line_end.x - line_start.x),
873            y: line_start.y + t1 * (line_end.y - line_start.y),
874        };
875        candidates.push((t1, point));
876    }
877    if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
878        let point = Coords2d {
879            x: line_start.x + t2 * (line_end.x - line_start.x),
880            y: line_start.y + t2 * (line_end.y - line_start.y),
881        };
882        candidates.push((t2, point));
883    }
884
885    candidates.retain(|(_, point)| is_point_on_arc(*point, arc_center, arc_start, arc_end, epsilon));
886    candidates.sort_by(|(a_t, _), (b_t, _)| a_t.partial_cmp(b_t).unwrap_or(std::cmp::Ordering::Equal));
887    candidates
888}
889
890/// Helper to calculate intersection between a line segment and an arc.
891///
892/// Returns the first intersection point if found, None otherwise.
893pub fn line_arc_intersection(
894    line_start: Coords2d,
895    line_end: Coords2d,
896    arc_center: Coords2d,
897    arc_start: Coords2d,
898    arc_end: Coords2d,
899    epsilon: f64,
900) -> Option<Coords2d> {
901    line_arc_intersections(line_start, line_end, arc_center, arc_start, arc_end, epsilon)
902        .into_iter()
903        .map(|(_, point)| point)
904        .next()
905}
906
907/// Helper to calculate intersection points between a line segment and a circle.
908///
909/// Returns intersections as `(t, point)` where `t` is the line parametric position:
910/// `point = line_start + t * (line_end - line_start)`, with `0 <= t <= 1`.
911pub fn line_circle_intersections(
912    line_start: Coords2d,
913    line_end: Coords2d,
914    circle_center: Coords2d,
915    radius: f64,
916    epsilon: f64,
917) -> Vec<(f64, Coords2d)> {
918    // Translate line to origin (center at 0,0)
919    let translated_line_start = Coords2d {
920        x: line_start.x - circle_center.x,
921        y: line_start.y - circle_center.y,
922    };
923    let translated_line_end = Coords2d {
924        x: line_end.x - circle_center.x,
925        y: line_end.y - circle_center.y,
926    };
927
928    let dx = translated_line_end.x - translated_line_start.x;
929    let dy = translated_line_end.y - translated_line_start.y;
930    let a = dx * dx + dy * dy;
931    let b = 2.0 * (translated_line_start.x * dx + translated_line_start.y * dy);
932    let c = translated_line_start.x * translated_line_start.x + translated_line_start.y * translated_line_start.y
933        - radius * radius;
934
935    if a.abs() < EPSILON_PARALLEL {
936        return Vec::new();
937    }
938
939    let discriminant = b * b - 4.0 * a * c;
940    if discriminant < 0.0 {
941        return Vec::new();
942    }
943
944    let sqrt_discriminant = discriminant.sqrt();
945    let mut intersections = Vec::new();
946
947    let t1 = (-b - sqrt_discriminant) / (2.0 * a);
948    if (0.0..=1.0).contains(&t1) {
949        intersections.push((
950            t1,
951            Coords2d {
952                x: line_start.x + t1 * (line_end.x - line_start.x),
953                y: line_start.y + t1 * (line_end.y - line_start.y),
954            },
955        ));
956    }
957
958    let t2 = (-b + sqrt_discriminant) / (2.0 * a);
959    if (0.0..=1.0).contains(&t2) && (t2 - t1).abs() > epsilon {
960        intersections.push((
961            t2,
962            Coords2d {
963                x: line_start.x + t2 * (line_end.x - line_start.x),
964                y: line_start.y + t2 * (line_end.y - line_start.y),
965            },
966        ));
967    }
968
969    intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
970    intersections
971}
972
973/// Parametric position of a point on a circle, measured CCW from the circle start point.
974///
975/// Returns `t` in `[0, 1)` where:
976/// - `t = 0` at circle start
977/// - increasing `t` moves CCW
978pub fn project_point_onto_circle(point: Coords2d, center: Coords2d, start: Coords2d) -> f64 {
979    let normalize_angle = |angle: f64| -> f64 {
980        if !angle.is_finite() {
981            return angle;
982        }
983        let mut normalized = angle;
984        while normalized < 0.0 {
985            normalized += TAU;
986        }
987        while normalized >= TAU {
988            normalized -= TAU;
989        }
990        normalized
991    };
992
993    let start_angle = normalize_angle(libm::atan2(start.y - center.y, start.x - center.x));
994    let point_angle = normalize_angle(libm::atan2(point.y - center.y, point.x - center.x));
995    let delta_ccw = (point_angle - start_angle).rem_euclid(TAU);
996    delta_ccw / TAU
997}
998
999fn is_point_on_circle(point: Coords2d, center: Coords2d, radius: f64, epsilon: f64) -> bool {
1000    let dist = ((point.x - center.x) * (point.x - center.x) + (point.y - center.y) * (point.y - center.y)).sqrt();
1001    (dist - radius).abs() <= epsilon
1002}
1003
1004/// Helper to calculate the parametric position of a point on an arc
1005/// Returns t where t=0 at start, t=1 at end, based on CCW angle
1006pub fn project_point_onto_arc(point: Coords2d, arc_center: Coords2d, arc_start: Coords2d, arc_end: Coords2d) -> f64 {
1007    // Calculate angles
1008    let start_angle = libm::atan2(arc_start.y - arc_center.y, arc_start.x - arc_center.x);
1009    let end_angle = libm::atan2(arc_end.y - arc_center.y, arc_end.x - arc_center.x);
1010    let point_angle = libm::atan2(point.y - arc_center.y, point.x - arc_center.x);
1011
1012    // Normalize angles to [0, 2Ï€]
1013    let normalize_angle = |angle: f64| -> f64 {
1014        if !angle.is_finite() {
1015            return angle;
1016        }
1017        let mut normalized = angle;
1018        while normalized < 0.0 {
1019            normalized += TAU;
1020        }
1021        while normalized >= TAU {
1022            normalized -= TAU;
1023        }
1024        normalized
1025    };
1026
1027    let normalized_start = normalize_angle(start_angle);
1028    let normalized_end = normalize_angle(end_angle);
1029    let normalized_point = normalize_angle(point_angle);
1030
1031    // Calculate arc length (CCW)
1032    let arc_length = if normalized_start < normalized_end {
1033        normalized_end - normalized_start
1034    } else {
1035        // Wrap around
1036        TAU - normalized_start + normalized_end
1037    };
1038
1039    if arc_length < EPSILON_PARALLEL {
1040        // Arc is degenerate (full circle or very small)
1041        return 0.0;
1042    }
1043
1044    // Calculate point's position along arc (CCW from start)
1045    let point_arc_length = if normalized_start < normalized_end {
1046        if normalized_point >= normalized_start && normalized_point <= normalized_end {
1047            normalized_point - normalized_start
1048        } else {
1049            // Point is not on the arc, return closest endpoint
1050            let dist_to_start = libm::fmin(
1051                (normalized_point - normalized_start).abs(),
1052                TAU - (normalized_point - normalized_start).abs(),
1053            );
1054            let dist_to_end = libm::fmin(
1055                (normalized_point - normalized_end).abs(),
1056                TAU - (normalized_point - normalized_end).abs(),
1057            );
1058            return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
1059        }
1060    } else {
1061        // Wrap around case
1062        if normalized_point >= normalized_start || normalized_point <= normalized_end {
1063            if normalized_point >= normalized_start {
1064                normalized_point - normalized_start
1065            } else {
1066                TAU - normalized_start + normalized_point
1067            }
1068        } else {
1069            // Point is not on the arc
1070            let dist_to_start = libm::fmin(
1071                (normalized_point - normalized_start).abs(),
1072                TAU - (normalized_point - normalized_start).abs(),
1073            );
1074            let dist_to_end = libm::fmin(
1075                (normalized_point - normalized_end).abs(),
1076                TAU - (normalized_point - normalized_end).abs(),
1077            );
1078            return if dist_to_start < dist_to_end { 0.0 } else { 1.0 };
1079        }
1080    };
1081
1082    // Return parametric position
1083    point_arc_length / arc_length
1084}
1085
1086/// Helper to calculate all intersections between two arcs (via circle-circle intersection).
1087///
1088/// Returns all valid points that lie on both arcs (0, 1, or 2).
1089pub fn arc_arc_intersections(
1090    arc1_center: Coords2d,
1091    arc1_start: Coords2d,
1092    arc1_end: Coords2d,
1093    arc2_center: Coords2d,
1094    arc2_start: Coords2d,
1095    arc2_end: Coords2d,
1096    epsilon: f64,
1097) -> Vec<Coords2d> {
1098    // Calculate radii
1099    let r1 = ((arc1_start.x - arc1_center.x) * (arc1_start.x - arc1_center.x)
1100        + (arc1_start.y - arc1_center.y) * (arc1_start.y - arc1_center.y))
1101        .sqrt();
1102    let r2 = ((arc2_start.x - arc2_center.x) * (arc2_start.x - arc2_center.x)
1103        + (arc2_start.y - arc2_center.y) * (arc2_start.y - arc2_center.y))
1104        .sqrt();
1105
1106    // Distance between centers
1107    let dx = arc2_center.x - arc1_center.x;
1108    let dy = arc2_center.y - arc1_center.y;
1109    let d = (dx * dx + dy * dy).sqrt();
1110
1111    // Check if circles intersect
1112    if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon {
1113        // No intersection
1114        return Vec::new();
1115    }
1116
1117    // Check for degenerate cases
1118    if d < EPSILON_PARALLEL {
1119        // Concentric circles - no intersection (or infinite if same radius, but we treat as none)
1120        return Vec::new();
1121    }
1122
1123    // Calculate intersection points
1124    // Using the formula from: https://mathworld.wolfram.com/Circle-CircleIntersection.html
1125    let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1126    let h_sq = r1 * r1 - a * a;
1127
1128    // If h_sq is negative, no intersection
1129    if h_sq < 0.0 {
1130        return Vec::new();
1131    }
1132
1133    let h = h_sq.sqrt();
1134
1135    // If h is NaN, no intersection
1136    if h.is_nan() {
1137        return Vec::new();
1138    }
1139
1140    // Unit vector from arc1Center to arc2Center
1141    let ux = dx / d;
1142    let uy = dy / d;
1143
1144    // Perpendicular vector (rotated 90 degrees)
1145    let px = -uy;
1146    let py = ux;
1147
1148    // Midpoint on the line connecting centers
1149    let mid_point = Coords2d {
1150        x: arc1_center.x + a * ux,
1151        y: arc1_center.y + a * uy,
1152    };
1153
1154    // Two intersection points
1155    let intersection1 = Coords2d {
1156        x: mid_point.x + h * px,
1157        y: mid_point.y + h * py,
1158    };
1159    let intersection2 = Coords2d {
1160        x: mid_point.x - h * px,
1161        y: mid_point.y - h * py,
1162    };
1163
1164    // Check which intersection point(s) are on both arcs
1165    let mut candidates: Vec<Coords2d> = Vec::new();
1166
1167    if is_point_on_arc(intersection1, arc1_center, arc1_start, arc1_end, epsilon)
1168        && is_point_on_arc(intersection1, arc2_center, arc2_start, arc2_end, epsilon)
1169    {
1170        candidates.push(intersection1);
1171    }
1172
1173    if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1174        // Only check second point if it's different from the first
1175        if is_point_on_arc(intersection2, arc1_center, arc1_start, arc1_end, epsilon)
1176            && is_point_on_arc(intersection2, arc2_center, arc2_start, arc2_end, epsilon)
1177        {
1178            candidates.push(intersection2);
1179        }
1180    }
1181
1182    candidates
1183}
1184
1185/// Helper to calculate one intersection between two arcs (if any).
1186///
1187/// This is kept for compatibility with existing call sites/tests that expect
1188/// a single optional intersection.
1189pub fn arc_arc_intersection(
1190    arc1_center: Coords2d,
1191    arc1_start: Coords2d,
1192    arc1_end: Coords2d,
1193    arc2_center: Coords2d,
1194    arc2_start: Coords2d,
1195    arc2_end: Coords2d,
1196    epsilon: f64,
1197) -> Option<Coords2d> {
1198    arc_arc_intersections(
1199        arc1_center,
1200        arc1_start,
1201        arc1_end,
1202        arc2_center,
1203        arc2_start,
1204        arc2_end,
1205        epsilon,
1206    )
1207    .first()
1208    .copied()
1209}
1210
1211/// Helper to calculate intersections between a full circle and an arc.
1212///
1213/// Returns all valid intersection points on the arc (0, 1, or 2).
1214pub fn circle_arc_intersections(
1215    circle_center: Coords2d,
1216    circle_radius: f64,
1217    arc_center: Coords2d,
1218    arc_start: Coords2d,
1219    arc_end: Coords2d,
1220    epsilon: f64,
1221) -> Vec<Coords2d> {
1222    let r1 = circle_radius;
1223    let r2 = ((arc_start.x - arc_center.x) * (arc_start.x - arc_center.x)
1224        + (arc_start.y - arc_center.y) * (arc_start.y - arc_center.y))
1225        .sqrt();
1226
1227    let dx = arc_center.x - circle_center.x;
1228    let dy = arc_center.y - circle_center.y;
1229    let d = (dx * dx + dy * dy).sqrt();
1230
1231    if d > r1 + r2 + epsilon || d < (r1 - r2).abs() - epsilon || d < EPSILON_PARALLEL {
1232        return Vec::new();
1233    }
1234
1235    let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d);
1236    let h_sq = r1 * r1 - a * a;
1237    if h_sq < 0.0 {
1238        return Vec::new();
1239    }
1240    let h = h_sq.sqrt();
1241    if h.is_nan() {
1242        return Vec::new();
1243    }
1244
1245    let ux = dx / d;
1246    let uy = dy / d;
1247    let px = -uy;
1248    let py = ux;
1249    let mid_point = Coords2d {
1250        x: circle_center.x + a * ux,
1251        y: circle_center.y + a * uy,
1252    };
1253
1254    let intersection1 = Coords2d {
1255        x: mid_point.x + h * px,
1256        y: mid_point.y + h * py,
1257    };
1258    let intersection2 = Coords2d {
1259        x: mid_point.x - h * px,
1260        y: mid_point.y - h * py,
1261    };
1262
1263    let mut intersections = Vec::new();
1264    if is_point_on_arc(intersection1, arc_center, arc_start, arc_end, epsilon) {
1265        intersections.push(intersection1);
1266    }
1267    if ((intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon)
1268        && is_point_on_arc(intersection2, arc_center, arc_start, arc_end, epsilon)
1269    {
1270        intersections.push(intersection2);
1271    }
1272    intersections
1273}
1274
1275/// Helper to calculate intersections between two full circles.
1276///
1277/// Returns 0, 1 (tangent), or 2 intersection points.
1278pub fn circle_circle_intersections(
1279    circle1_center: Coords2d,
1280    circle1_radius: f64,
1281    circle2_center: Coords2d,
1282    circle2_radius: f64,
1283    epsilon: f64,
1284) -> Vec<Coords2d> {
1285    let dx = circle2_center.x - circle1_center.x;
1286    let dy = circle2_center.y - circle1_center.y;
1287    let d = (dx * dx + dy * dy).sqrt();
1288
1289    if d > circle1_radius + circle2_radius + epsilon
1290        || d < (circle1_radius - circle2_radius).abs() - epsilon
1291        || d < EPSILON_PARALLEL
1292    {
1293        return Vec::new();
1294    }
1295
1296    let a = (circle1_radius * circle1_radius - circle2_radius * circle2_radius + d * d) / (2.0 * d);
1297    let h_sq = circle1_radius * circle1_radius - a * a;
1298    if h_sq < 0.0 {
1299        return Vec::new();
1300    }
1301
1302    let h = if h_sq <= epsilon { 0.0 } else { h_sq.sqrt() };
1303    if h.is_nan() {
1304        return Vec::new();
1305    }
1306
1307    let ux = dx / d;
1308    let uy = dy / d;
1309    let px = -uy;
1310    let py = ux;
1311
1312    let mid_point = Coords2d {
1313        x: circle1_center.x + a * ux,
1314        y: circle1_center.y + a * uy,
1315    };
1316
1317    let intersection1 = Coords2d {
1318        x: mid_point.x + h * px,
1319        y: mid_point.y + h * py,
1320    };
1321    let intersection2 = Coords2d {
1322        x: mid_point.x - h * px,
1323        y: mid_point.y - h * py,
1324    };
1325
1326    let mut intersections = vec![intersection1];
1327    if (intersection1.x - intersection2.x).abs() > epsilon || (intersection1.y - intersection2.y).abs() > epsilon {
1328        intersections.push(intersection2);
1329    }
1330    intersections
1331}
1332
1333/// Helper to extract coordinates from a point object in JSON format
1334// Native type helper - get point coordinates from ObjectId
1335fn get_point_coords_from_native(objects: &[Object], point_id: ObjectId, default_unit: UnitLength) -> Option<Coords2d> {
1336    let point_obj = objects.get(point_id.0)?;
1337
1338    // Check if it's a Point segment
1339    let ObjectKind::Segment { segment } = &point_obj.kind else {
1340        return None;
1341    };
1342
1343    let Segment::Point(point) = segment else {
1344        return None;
1345    };
1346
1347    // Extract position coordinates in the trim internal unit
1348    Some(Coords2d {
1349        x: number_to_unit(&point.position.x, default_unit),
1350        y: number_to_unit(&point.position.y, default_unit),
1351    })
1352}
1353
1354// Legacy JSON helper (will be removed)
1355/// Helper to get point coordinates from a Line segment by looking up the point object (native types)
1356pub fn get_position_coords_for_line(
1357    segment_obj: &Object,
1358    which: LineEndpoint,
1359    objects: &[Object],
1360    default_unit: UnitLength,
1361) -> Option<Coords2d> {
1362    let ObjectKind::Segment { segment } = &segment_obj.kind else {
1363        return None;
1364    };
1365
1366    let Segment::Line(line) = segment else {
1367        return None;
1368    };
1369
1370    // Get the point ID from the segment
1371    let point_id = match which {
1372        LineEndpoint::Start => line.start,
1373        LineEndpoint::End => line.end,
1374    };
1375
1376    get_point_coords_from_native(objects, point_id, default_unit)
1377}
1378
1379/// Helper to check if a point is coincident with a segment (line or arc) via constraints (native types)
1380fn is_point_coincident_with_segment_native(point_id: ObjectId, segment_id: ObjectId, objects: &[Object]) -> bool {
1381    // Find coincident constraints
1382    for obj in objects {
1383        let ObjectKind::Constraint { constraint } = &obj.kind else {
1384            continue;
1385        };
1386
1387        let Constraint::Coincident(coincident) = constraint else {
1388            continue;
1389        };
1390
1391        // Check if both pointId and segmentId are in the segments array
1392        let has_point = coincident.contains_segment(point_id);
1393        let has_segment = coincident.contains_segment(segment_id);
1394
1395        if has_point && has_segment {
1396            return true;
1397        }
1398    }
1399    false
1400}
1401
1402/// Helper to get point coordinates from an Arc segment by looking up the point object (native types)
1403pub fn get_position_coords_from_arc(
1404    segment_obj: &Object,
1405    which: ArcPoint,
1406    objects: &[Object],
1407    default_unit: UnitLength,
1408) -> Option<Coords2d> {
1409    let ObjectKind::Segment { segment } = &segment_obj.kind else {
1410        return None;
1411    };
1412
1413    let Segment::Arc(arc) = segment else {
1414        return None;
1415    };
1416
1417    // Get the point ID from the segment
1418    let point_id = match which {
1419        ArcPoint::Start => arc.start,
1420        ArcPoint::End => arc.end,
1421        ArcPoint::Center => arc.center,
1422    };
1423
1424    get_point_coords_from_native(objects, point_id, default_unit)
1425}
1426
1427/// Helper to get point coordinates from a Circle segment by looking up the point object (native types)
1428pub fn get_position_coords_from_circle(
1429    segment_obj: &Object,
1430    which: CirclePoint,
1431    objects: &[Object],
1432    default_unit: UnitLength,
1433) -> Option<Coords2d> {
1434    let ObjectKind::Segment { segment } = &segment_obj.kind else {
1435        return None;
1436    };
1437
1438    let Segment::Circle(circle) = segment else {
1439        return None;
1440    };
1441
1442    let point_id = match which {
1443        CirclePoint::Start => circle.start,
1444        CirclePoint::Center => circle.center,
1445    };
1446
1447    get_point_coords_from_native(objects, point_id, default_unit)
1448}
1449
1450/// Internal normalized curve kind used by trim.
1451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1452enum CurveKind {
1453    Line,
1454    Circular,
1455}
1456
1457/// Internal curve domain used by trim.
1458#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1459enum CurveDomain {
1460    Open,
1461    Closed,
1462}
1463
1464/// Internal normalized curve representation loaded from a scene segment.
1465#[derive(Debug, Clone, Copy)]
1466struct CurveHandle {
1467    segment_id: ObjectId,
1468    kind: CurveKind,
1469    domain: CurveDomain,
1470    start: Coords2d,
1471    end: Coords2d,
1472    center: Option<Coords2d>,
1473    radius: Option<f64>,
1474}
1475
1476impl CurveHandle {
1477    fn project_for_trim(self, point: Coords2d) -> Result<f64, String> {
1478        match (self.kind, self.domain) {
1479            (CurveKind::Line, CurveDomain::Open) => Ok(project_point_onto_segment(point, self.start, self.end)),
1480            (CurveKind::Circular, CurveDomain::Open) => {
1481                let center = self
1482                    .center
1483                    .ok_or_else(|| format!("Curve {} missing center for arc projection", self.segment_id.0))?;
1484                Ok(project_point_onto_arc(point, center, self.start, self.end))
1485            }
1486            (CurveKind::Circular, CurveDomain::Closed) => {
1487                let center = self
1488                    .center
1489                    .ok_or_else(|| format!("Curve {} missing center for circle projection", self.segment_id.0))?;
1490                Ok(project_point_onto_circle(point, center, self.start))
1491            }
1492            (CurveKind::Line, CurveDomain::Closed) => Err(format!(
1493                "Invalid curve state: line {} cannot be closed",
1494                self.segment_id.0
1495            )),
1496        }
1497    }
1498}
1499
1500/// Load a normalized curve handle from a segment object.
1501fn load_curve_handle(
1502    segment_obj: &Object,
1503    objects: &[Object],
1504    default_unit: UnitLength,
1505) -> Result<CurveHandle, String> {
1506    let ObjectKind::Segment { segment } = &segment_obj.kind else {
1507        return Err("Object is not a segment".to_owned());
1508    };
1509
1510    match segment {
1511        Segment::Line(_) => {
1512            let start = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit)
1513                .ok_or_else(|| format!("Could not get line start for segment {}", segment_obj.id.0))?;
1514            let end = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit)
1515                .ok_or_else(|| format!("Could not get line end for segment {}", segment_obj.id.0))?;
1516            Ok(CurveHandle {
1517                segment_id: segment_obj.id,
1518                kind: CurveKind::Line,
1519                domain: CurveDomain::Open,
1520                start,
1521                end,
1522                center: None,
1523                radius: None,
1524            })
1525        }
1526        Segment::Arc(_) => {
1527            let start = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit)
1528                .ok_or_else(|| format!("Could not get arc start for segment {}", segment_obj.id.0))?;
1529            let end = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit)
1530                .ok_or_else(|| format!("Could not get arc end for segment {}", segment_obj.id.0))?;
1531            let center = get_position_coords_from_arc(segment_obj, ArcPoint::Center, objects, default_unit)
1532                .ok_or_else(|| format!("Could not get arc center for segment {}", segment_obj.id.0))?;
1533            let radius =
1534                ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1535            Ok(CurveHandle {
1536                segment_id: segment_obj.id,
1537                kind: CurveKind::Circular,
1538                domain: CurveDomain::Open,
1539                start,
1540                end,
1541                center: Some(center),
1542                radius: Some(radius),
1543            })
1544        }
1545        Segment::Circle(_) => {
1546            let start = get_position_coords_from_circle(segment_obj, CirclePoint::Start, objects, default_unit)
1547                .ok_or_else(|| format!("Could not get circle start for segment {}", segment_obj.id.0))?;
1548            let center = get_position_coords_from_circle(segment_obj, CirclePoint::Center, objects, default_unit)
1549                .ok_or_else(|| format!("Could not get circle center for segment {}", segment_obj.id.0))?;
1550            let radius =
1551                ((start.x - center.x) * (start.x - center.x) + (start.y - center.y) * (start.y - center.y)).sqrt();
1552            Ok(CurveHandle {
1553                segment_id: segment_obj.id,
1554                kind: CurveKind::Circular,
1555                domain: CurveDomain::Closed,
1556                start,
1557                // Closed curves have no true "end"; keep current trim semantics by mirroring start.
1558                end: start,
1559                center: Some(center),
1560                radius: Some(radius),
1561            })
1562        }
1563        Segment::Point(_) => Err(format!(
1564            "Point segment {} cannot be used as trim curve",
1565            segment_obj.id.0
1566        )),
1567    }
1568}
1569
1570fn project_point_onto_curve(curve: CurveHandle, point: Coords2d) -> Result<f64, String> {
1571    curve.project_for_trim(point)
1572}
1573
1574fn curve_contains_point(curve: CurveHandle, point: Coords2d, epsilon: f64) -> bool {
1575    match (curve.kind, curve.domain) {
1576        (CurveKind::Line, CurveDomain::Open) => {
1577            let t = project_point_onto_segment(point, curve.start, curve.end);
1578            (0.0..=1.0).contains(&t) && perpendicular_distance_to_segment(point, curve.start, curve.end) <= epsilon
1579        }
1580        (CurveKind::Circular, CurveDomain::Open) => curve
1581            .center
1582            .is_some_and(|center| is_point_on_arc(point, center, curve.start, curve.end, epsilon)),
1583        (CurveKind::Circular, CurveDomain::Closed) => curve.center.is_some_and(|center| {
1584            let radius = curve
1585                .radius
1586                .unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
1587            is_point_on_circle(point, center, radius, epsilon)
1588        }),
1589        (CurveKind::Line, CurveDomain::Closed) => false,
1590    }
1591}
1592
1593fn curve_line_segment_intersections(
1594    curve: CurveHandle,
1595    line_start: Coords2d,
1596    line_end: Coords2d,
1597    epsilon: f64,
1598) -> Vec<(f64, Coords2d)> {
1599    match (curve.kind, curve.domain) {
1600        (CurveKind::Line, CurveDomain::Open) => {
1601            line_segment_intersection(line_start, line_end, curve.start, curve.end, epsilon)
1602                .map(|intersection| {
1603                    (
1604                        project_point_onto_segment(intersection, line_start, line_end),
1605                        intersection,
1606                    )
1607                })
1608                .into_iter()
1609                .collect()
1610        }
1611        (CurveKind::Circular, CurveDomain::Open) => curve
1612            .center
1613            .map(|center| line_arc_intersections(line_start, line_end, center, curve.start, curve.end, epsilon))
1614            .unwrap_or_default(),
1615        (CurveKind::Circular, CurveDomain::Closed) => {
1616            let Some(center) = curve.center else {
1617                return Vec::new();
1618            };
1619            let radius = curve
1620                .radius
1621                .unwrap_or_else(|| ((curve.start.x - center.x).powi(2) + (curve.start.y - center.y).powi(2)).sqrt());
1622            line_circle_intersections(line_start, line_end, center, radius, epsilon)
1623        }
1624        (CurveKind::Line, CurveDomain::Closed) => Vec::new(),
1625    }
1626}
1627
1628fn curve_polyline_intersections(curve: CurveHandle, polyline: &[Coords2d], epsilon: f64) -> Vec<(Coords2d, usize)> {
1629    let mut intersections = Vec::new();
1630
1631    for i in 0..polyline.len().saturating_sub(1) {
1632        let p1 = polyline[i];
1633        let p2 = polyline[i + 1];
1634        for (_, intersection) in curve_line_segment_intersections(curve, p1, p2, epsilon) {
1635            intersections.push((intersection, i));
1636        }
1637    }
1638
1639    intersections
1640}
1641
1642fn curve_curve_intersections(curve: CurveHandle, other: CurveHandle, epsilon: f64) -> Vec<Coords2d> {
1643    match (curve.kind, curve.domain, other.kind, other.domain) {
1644        (CurveKind::Line, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => {
1645            line_segment_intersection(curve.start, curve.end, other.start, other.end, epsilon)
1646                .into_iter()
1647                .collect()
1648        }
1649        (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => other
1650            .center
1651            .map(|other_center| {
1652                line_arc_intersections(curve.start, curve.end, other_center, other.start, other.end, epsilon)
1653                    .into_iter()
1654                    .map(|(_, point)| point)
1655                    .collect()
1656            })
1657            .unwrap_or_default(),
1658        (CurveKind::Line, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
1659            let Some(other_center) = other.center else {
1660                return Vec::new();
1661            };
1662            let other_radius = other.radius.unwrap_or_else(|| {
1663                ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1664            });
1665            line_circle_intersections(curve.start, curve.end, other_center, other_radius, epsilon)
1666                .into_iter()
1667                .map(|(_, point)| point)
1668                .collect()
1669        }
1670        (CurveKind::Circular, CurveDomain::Open, CurveKind::Line, CurveDomain::Open) => curve
1671            .center
1672            .map(|curve_center| {
1673                line_arc_intersections(other.start, other.end, curve_center, curve.start, curve.end, epsilon)
1674                    .into_iter()
1675                    .map(|(_, point)| point)
1676                    .collect()
1677            })
1678            .unwrap_or_default(),
1679        (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Open) => {
1680            let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1681                return Vec::new();
1682            };
1683            arc_arc_intersections(
1684                curve_center,
1685                curve.start,
1686                curve.end,
1687                other_center,
1688                other.start,
1689                other.end,
1690                epsilon,
1691            )
1692        }
1693        (CurveKind::Circular, CurveDomain::Open, CurveKind::Circular, CurveDomain::Closed) => {
1694            let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1695                return Vec::new();
1696            };
1697            let other_radius = other.radius.unwrap_or_else(|| {
1698                ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1699            });
1700            circle_arc_intersections(
1701                other_center,
1702                other_radius,
1703                curve_center,
1704                curve.start,
1705                curve.end,
1706                epsilon,
1707            )
1708        }
1709        (CurveKind::Circular, CurveDomain::Closed, CurveKind::Line, CurveDomain::Open) => {
1710            let Some(curve_center) = curve.center else {
1711                return Vec::new();
1712            };
1713            let curve_radius = curve.radius.unwrap_or_else(|| {
1714                ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1715            });
1716            line_circle_intersections(other.start, other.end, curve_center, curve_radius, epsilon)
1717                .into_iter()
1718                .map(|(_, point)| point)
1719                .collect()
1720        }
1721        (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Open) => {
1722            let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1723                return Vec::new();
1724            };
1725            let curve_radius = curve.radius.unwrap_or_else(|| {
1726                ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1727            });
1728            circle_arc_intersections(
1729                curve_center,
1730                curve_radius,
1731                other_center,
1732                other.start,
1733                other.end,
1734                epsilon,
1735            )
1736        }
1737        (CurveKind::Circular, CurveDomain::Closed, CurveKind::Circular, CurveDomain::Closed) => {
1738            let (Some(curve_center), Some(other_center)) = (curve.center, other.center) else {
1739                return Vec::new();
1740            };
1741            let curve_radius = curve.radius.unwrap_or_else(|| {
1742                ((curve.start.x - curve_center.x).powi(2) + (curve.start.y - curve_center.y).powi(2)).sqrt()
1743            });
1744            let other_radius = other.radius.unwrap_or_else(|| {
1745                ((other.start.x - other_center.x).powi(2) + (other.start.y - other_center.y).powi(2)).sqrt()
1746            });
1747            circle_circle_intersections(curve_center, curve_radius, other_center, other_radius, epsilon)
1748        }
1749        _ => Vec::new(),
1750    }
1751}
1752
1753fn segment_endpoint_points(
1754    segment_obj: &Object,
1755    objects: &[Object],
1756    default_unit: UnitLength,
1757) -> Vec<(ObjectId, Coords2d)> {
1758    let ObjectKind::Segment { segment } = &segment_obj.kind else {
1759        return Vec::new();
1760    };
1761
1762    match segment {
1763        Segment::Line(line) => {
1764            let mut points = Vec::new();
1765            if let Some(start) = get_position_coords_for_line(segment_obj, LineEndpoint::Start, objects, default_unit) {
1766                points.push((line.start, start));
1767            }
1768            if let Some(end) = get_position_coords_for_line(segment_obj, LineEndpoint::End, objects, default_unit) {
1769                points.push((line.end, end));
1770            }
1771            points
1772        }
1773        Segment::Arc(arc) => {
1774            let mut points = Vec::new();
1775            if let Some(start) = get_position_coords_from_arc(segment_obj, ArcPoint::Start, objects, default_unit) {
1776                points.push((arc.start, start));
1777            }
1778            if let Some(end) = get_position_coords_from_arc(segment_obj, ArcPoint::End, objects, default_unit) {
1779                points.push((arc.end, end));
1780            }
1781            points
1782        }
1783        _ => Vec::new(),
1784    }
1785}
1786
1787/// Find the next trim spawn (intersection) between trim line and scene segments
1788///
1789/// When a user draws a trim line, we loop over each pairs of points of the trim line,
1790/// until we find an intersection, this intersection is called the trim spawn (to differentiate from
1791/// segment-segment intersections which are also important for trimming).
1792/// Below the dashes are segments and the periods are points on the trim line.
1793///
1794/// ```
1795///          /
1796///         /
1797///        /    .
1798/// ------/-------x--------
1799///      /       .       
1800///     /       .       
1801///    /           .   
1802/// ```
1803///
1804/// When we find a trim spawn we stop looping but save the index as we process each trim spawn one at a time.
1805/// The loop that processes each spawn one at a time is managed by `execute_trim_loop` (or `execute_trim_loop_with_context`).
1806///
1807/// Loops through polyline segments starting from startIndex and checks for intersections
1808/// with all scene segments (both Line and Arc). Returns the first intersection found.
1809///
1810/// **Units:** Trim line points are expected in millimeters at the API boundary. Callers should
1811/// normalize points to the current/default length unit before calling this function (the
1812/// trim loop does this for you). Segment positions read from `objects` are converted to that same
1813/// unit internally.
1814pub fn get_next_trim_spawn(
1815    points: &[Coords2d],
1816    start_index: usize,
1817    objects: &[Object],
1818    default_unit: UnitLength,
1819) -> TrimItem {
1820    let scene_curves: Vec<CurveHandle> = objects
1821        .iter()
1822        .filter_map(|obj| load_curve_handle(obj, objects, default_unit).ok())
1823        .collect();
1824
1825    // Loop through polyline segments starting from startIndex
1826    for i in start_index..points.len().saturating_sub(1) {
1827        let p1 = points[i];
1828        let p2 = points[i + 1];
1829
1830        // Check this polyline segment against all scene segments
1831        for curve in &scene_curves {
1832            let intersections = curve_line_segment_intersections(*curve, p1, p2, EPSILON_POINT_ON_SEGMENT);
1833            if let Some((_, intersection)) = intersections.first() {
1834                return TrimItem::Spawn {
1835                    trim_spawn_seg_id: curve.segment_id,
1836                    trim_spawn_coords: *intersection,
1837                    next_index: i,
1838                };
1839            }
1840        }
1841    }
1842
1843    // No intersection found
1844    TrimItem::None {
1845        next_index: points.len().saturating_sub(1),
1846    }
1847}
1848
1849/**
1850 * For the trim spawn segment and the intersection point on that segment,
1851 * finds the "trim terminations" in both directions (left and right from the intersection point).
1852 * A trim termination is the point where trimming should stop in each direction.
1853 *
1854 * The function searches for candidates in each direction and selects the closest one,
1855 * with the following priority when distances are equal: coincident > intersection > endpoint.
1856 *
1857 * ## segEndPoint: The segment's own endpoint
1858 *
1859 *   ========0
1860 * OR
1861 *   ========0
1862 *            \
1863 *             \
1864 *
1865 *  Returns this when:
1866 *  - No other candidates are found between the intersection point and the segment end
1867 *  - An intersection is found at the segment's own endpoint (even if due to numerical precision)
1868 *  - An intersection is found at another segment's endpoint (without a coincident constraint)
1869 *  - The closest candidate is the segment's own endpoint
1870 *
1871 * ## intersection: Intersection with another segment's body
1872 *            /
1873 *           /
1874 *  ========X=====
1875 *         /
1876 *        /
1877 *
1878 *  Returns this when:
1879 *  - A geometric intersection is found with another segment's body (not at an endpoint)
1880 *  - The intersection is not at our own segment's endpoint
1881 *  - The intersection is not at the other segment's endpoint (which would be segEndPoint)
1882 *
1883 * ## trimSpawnSegmentCoincidentWithAnotherSegmentPoint: Another segment's endpoint coincident with our segment
1884 *
1885 *  ========0=====
1886 *         /
1887 *        /
1888 *
1889 *  Returns this when:
1890 *  - Another segment's endpoint has a coincident constraint with our trim spawn segment
1891 *  - The endpoint's perpendicular distance to our segment is within epsilon
1892 *  - The endpoint is geometrically on our segment (between start and end)
1893 *  - This takes priority over intersections when distances are equal (within epsilon)
1894 *
1895 * ## Fallback
1896 *  If no candidates are found in a direction, defaults to "segEndPoint".
1897 * */
1898/// Find trim terminations for both sides of a trim spawn
1899///
1900/// For the trim spawn segment and the intersection point on that segment,
1901/// finds the "trim terminations" in both directions (left and right from the intersection point).
1902/// A trim termination is the point where trimming should stop in each direction.
1903pub fn get_trim_spawn_terminations(
1904    trim_spawn_seg_id: ObjectId,
1905    trim_spawn_coords: &[Coords2d],
1906    objects: &[Object],
1907    default_unit: UnitLength,
1908) -> Result<TrimTerminations, String> {
1909    // Find the trim spawn segment
1910    let trim_spawn_seg = objects.iter().find(|obj| obj.id == trim_spawn_seg_id);
1911
1912    let trim_spawn_seg = match trim_spawn_seg {
1913        Some(seg) => seg,
1914        None => {
1915            return Err(format!("Trim spawn segment {} not found", trim_spawn_seg_id.0));
1916        }
1917    };
1918
1919    let trim_curve = load_curve_handle(trim_spawn_seg, objects, default_unit).map_err(|e| {
1920        format!(
1921            "Failed to load trim spawn segment {} as normalized curve: {}",
1922            trim_spawn_seg_id.0, e
1923        )
1924    })?;
1925
1926    // Find intersection point between polyline and trim spawn segment
1927    // trimSpawnCoords is a polyline, so we check each segment
1928    // We need to find ALL intersections and use a consistent one to avoid
1929    // different results for different trim lines in the same area
1930    let all_intersections = curve_polyline_intersections(trim_curve, trim_spawn_coords, EPSILON_POINT_ON_SEGMENT);
1931
1932    // Use the intersection that's closest to the middle of the polyline
1933    // This ensures consistent results regardless of which segment intersects first
1934    let intersection_point = if all_intersections.is_empty() {
1935        return Err("Could not find intersection point between polyline and trim spawn segment".to_string());
1936    } else {
1937        // Find the middle of the polyline
1938        let mid_index = (trim_spawn_coords.len() - 1) / 2;
1939        let mid_point = trim_spawn_coords[mid_index];
1940
1941        // Find the intersection closest to the middle
1942        let mut min_dist = f64::INFINITY;
1943        let mut closest_intersection = all_intersections[0].0;
1944
1945        for (intersection, _) in &all_intersections {
1946            let dist = ((intersection.x - mid_point.x) * (intersection.x - mid_point.x)
1947                + (intersection.y - mid_point.y) * (intersection.y - mid_point.y))
1948                .sqrt();
1949            if dist < min_dist {
1950                min_dist = dist;
1951                closest_intersection = *intersection;
1952            }
1953        }
1954
1955        closest_intersection
1956    };
1957
1958    // Project intersection point onto segment to get parametric position
1959    let intersection_t = project_point_onto_curve(trim_curve, intersection_point)?;
1960
1961    // Find terminations on both sides
1962    let left_termination = find_termination_in_direction(
1963        trim_spawn_seg,
1964        trim_curve,
1965        intersection_t,
1966        TrimDirection::Left,
1967        objects,
1968        default_unit,
1969    )?;
1970
1971    let right_termination = find_termination_in_direction(
1972        trim_spawn_seg,
1973        trim_curve,
1974        intersection_t,
1975        TrimDirection::Right,
1976        objects,
1977        default_unit,
1978    )?;
1979
1980    Ok(TrimTerminations {
1981        left_side: left_termination,
1982        right_side: right_termination,
1983    })
1984}
1985
1986/// Helper to find trim termination in a given direction from the intersection point
1987///
1988/// This is called by `get_trim_spawn_terminations` for each direction (left and right).
1989/// It searches for candidates in the specified direction and selects the closest one,
1990/// with the following priority when distances are equal: coincident > intersection > endpoint.
1991///
1992/// ## segEndPoint: The segment's own endpoint
1993///
1994/// ```
1995///   ========0
1996/// OR
1997///   ========0
1998///            \
1999///             \
2000/// ```
2001///
2002/// Returns this when:
2003/// - No other candidates are found between the intersection point and the segment end
2004/// - An intersection is found at the segment's own endpoint (even if due to numerical precision)
2005/// - An intersection is found at another segment's endpoint (without a coincident constraint)
2006/// - The closest candidate is the segment's own endpoint
2007///
2008/// ## intersection: Intersection with another segment's body
2009/// ```
2010///            /
2011///           /
2012///  ========X=====
2013///         /
2014///        /
2015/// ```
2016///
2017/// Returns this when:
2018/// - A geometric intersection is found with another segment's body (not at an endpoint)
2019/// - The intersection is not at our own segment's endpoint
2020/// - The intersection is not at the other segment's endpoint (which would be segEndPoint)
2021///
2022/// ## trimSpawnSegmentCoincidentWithAnotherSegmentPoint: Another segment's endpoint coincident with our segment
2023///
2024/// ```
2025///  ========0=====
2026///         /
2027///        /
2028/// ```
2029///
2030/// Returns this when:
2031/// - Another segment's endpoint has a coincident constraint with our trim spawn segment
2032/// - The endpoint's perpendicular distance to our segment is within epsilon
2033/// - The endpoint is geometrically on our segment (between start and end)
2034/// - This takes priority over intersections when distances are equal (within epsilon)
2035///
2036/// ## Fallback
2037/// If no candidates are found in a direction, defaults to "segEndPoint".
2038fn find_termination_in_direction(
2039    trim_spawn_seg: &Object,
2040    trim_curve: CurveHandle,
2041    intersection_t: f64,
2042    direction: TrimDirection,
2043    objects: &[Object],
2044    default_unit: UnitLength,
2045) -> Result<TrimTermination, String> {
2046    // Use native types
2047    let ObjectKind::Segment { segment } = &trim_spawn_seg.kind else {
2048        return Err("Trim spawn segment is not a segment".to_string());
2049    };
2050
2051    // Collect all candidate points: intersections, coincident points, and endpoints
2052    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2053    enum CandidateType {
2054        Intersection,
2055        Coincident,
2056        Endpoint,
2057    }
2058
2059    #[derive(Debug, Clone)]
2060    struct Candidate {
2061        t: f64,
2062        point: Coords2d,
2063        candidate_type: CandidateType,
2064        segment_id: Option<ObjectId>,
2065        point_id: Option<ObjectId>,
2066    }
2067
2068    let mut candidates: Vec<Candidate> = Vec::new();
2069
2070    // Add segment endpoints using native types
2071    match segment {
2072        Segment::Line(line) => {
2073            candidates.push(Candidate {
2074                t: 0.0,
2075                point: trim_curve.start,
2076                candidate_type: CandidateType::Endpoint,
2077                segment_id: None,
2078                point_id: Some(line.start),
2079            });
2080            candidates.push(Candidate {
2081                t: 1.0,
2082                point: trim_curve.end,
2083                candidate_type: CandidateType::Endpoint,
2084                segment_id: None,
2085                point_id: Some(line.end),
2086            });
2087        }
2088        Segment::Arc(arc) => {
2089            // For arcs, endpoints are at t=0 and t=1 conceptually
2090            candidates.push(Candidate {
2091                t: 0.0,
2092                point: trim_curve.start,
2093                candidate_type: CandidateType::Endpoint,
2094                segment_id: None,
2095                point_id: Some(arc.start),
2096            });
2097            candidates.push(Candidate {
2098                t: 1.0,
2099                point: trim_curve.end,
2100                candidate_type: CandidateType::Endpoint,
2101                segment_id: None,
2102                point_id: Some(arc.end),
2103            });
2104        }
2105        Segment::Circle(_) => {
2106            // Circles have no endpoints for trim termination purposes.
2107        }
2108        _ => {}
2109    }
2110
2111    // Get trim spawn segment ID for comparison
2112    let trim_spawn_seg_id = trim_spawn_seg.id;
2113
2114    // Find intersections and coincident endpoint candidates against all other segments.
2115    for other_seg in objects.iter() {
2116        let other_id = other_seg.id;
2117        if other_id == trim_spawn_seg_id {
2118            continue;
2119        }
2120
2121        if let Ok(other_curve) = load_curve_handle(other_seg, objects, default_unit) {
2122            for intersection in curve_curve_intersections(trim_curve, other_curve, EPSILON_POINT_ON_SEGMENT) {
2123                let Ok(t) = project_point_onto_curve(trim_curve, intersection) else {
2124                    continue;
2125                };
2126                candidates.push(Candidate {
2127                    t,
2128                    point: intersection,
2129                    candidate_type: CandidateType::Intersection,
2130                    segment_id: Some(other_id),
2131                    point_id: None,
2132                });
2133            }
2134        }
2135
2136        for (other_point_id, other_point) in segment_endpoint_points(other_seg, objects, default_unit) {
2137            if !is_point_coincident_with_segment_native(other_point_id, trim_spawn_seg_id, objects) {
2138                continue;
2139            }
2140            if !curve_contains_point(trim_curve, other_point, EPSILON_POINT_ON_SEGMENT) {
2141                continue;
2142            }
2143            let Ok(t) = project_point_onto_curve(trim_curve, other_point) else {
2144                continue;
2145            };
2146            candidates.push(Candidate {
2147                t,
2148                point: other_point,
2149                candidate_type: CandidateType::Coincident,
2150                segment_id: Some(other_id),
2151                point_id: Some(other_point_id),
2152            });
2153        }
2154    }
2155
2156    let is_circle_segment = trim_curve.domain == CurveDomain::Closed;
2157
2158    // Filter candidates to exclude the intersection point itself and those on the wrong side.
2159    // Use a slightly larger epsilon to account for numerical precision variations.
2160    let intersection_epsilon = EPSILON_POINT_ON_SEGMENT * 10.0; // 0.0001mm
2161    let direction_distance = |candidate_t: f64| -> f64 {
2162        if is_circle_segment {
2163            match direction {
2164                TrimDirection::Left => (intersection_t - candidate_t).rem_euclid(1.0),
2165                TrimDirection::Right => (candidate_t - intersection_t).rem_euclid(1.0),
2166            }
2167        } else {
2168            (candidate_t - intersection_t).abs()
2169        }
2170    };
2171    let filtered_candidates: Vec<Candidate> = candidates
2172        .into_iter()
2173        .filter(|candidate| {
2174            let dist_from_intersection = if is_circle_segment {
2175                let ccw = (candidate.t - intersection_t).rem_euclid(1.0);
2176                let cw = (intersection_t - candidate.t).rem_euclid(1.0);
2177                libm::fmin(ccw, cw)
2178            } else {
2179                (candidate.t - intersection_t).abs()
2180            };
2181            if dist_from_intersection < intersection_epsilon {
2182                return false; // Too close to intersection point
2183            }
2184
2185            if is_circle_segment {
2186                direction_distance(candidate.t) > intersection_epsilon
2187            } else {
2188                match direction {
2189                    TrimDirection::Left => candidate.t < intersection_t,
2190                    TrimDirection::Right => candidate.t > intersection_t,
2191                }
2192            }
2193        })
2194        .collect();
2195
2196    // Sort candidates by distance from intersection (closest first).
2197    // When distances are equal, prioritize: coincident > intersection > endpoint.
2198    // Also allow constrained coincident endpoints to win over nearby geometric
2199    // intersections so arc endpoints coincident with line segments terminate at
2200    // the authored endpoint instead of a tiny adjacent arc-body intersection.
2201    let mut sorted_candidates = filtered_candidates;
2202    sorted_candidates.sort_by(|a, b| {
2203        let dist_a = direction_distance(a.t);
2204        let dist_b = direction_distance(b.t);
2205        let dist_diff = dist_a - dist_b;
2206        let coincident_snap_applies = dist_diff.abs() <= EPSILON_COINCIDENT_TERMINATION_SNAP
2207            && (a.candidate_type == CandidateType::Coincident || b.candidate_type == CandidateType::Coincident);
2208        if dist_diff.abs() > EPSILON_POINT_ON_SEGMENT && !coincident_snap_applies {
2209            dist_diff.partial_cmp(&0.0).unwrap_or(std::cmp::Ordering::Equal)
2210        } else {
2211            // Distances are effectively equal - prioritize by type
2212            let type_priority = |candidate_type: CandidateType| -> i32 {
2213                match candidate_type {
2214                    CandidateType::Coincident => 0,
2215                    CandidateType::Intersection => 1,
2216                    CandidateType::Endpoint => 2,
2217                }
2218            };
2219            type_priority(a.candidate_type).cmp(&type_priority(b.candidate_type))
2220        }
2221    });
2222
2223    // Find the first valid trim termination
2224    let closest_candidate = match sorted_candidates.first() {
2225        Some(c) => c,
2226        None => {
2227            if is_circle_segment {
2228                return Err("No trim termination candidate found for circle".to_string());
2229            }
2230            // No trim termination found, default to segment endpoint
2231            let endpoint = match direction {
2232                TrimDirection::Left => trim_curve.start,
2233                TrimDirection::Right => trim_curve.end,
2234            };
2235            return Ok(TrimTermination::SegEndPoint {
2236                trim_termination_coords: endpoint,
2237            });
2238        }
2239    };
2240
2241    // Check if the closest candidate is an intersection that is actually another segment's endpoint
2242    // According to test case: if another segment's endpoint is on our segment (even without coincident constraint),
2243    // we should return segEndPoint, not intersection
2244    if !is_circle_segment
2245        && closest_candidate.candidate_type == CandidateType::Intersection
2246        && let Some(seg_id) = closest_candidate.segment_id
2247    {
2248        let intersecting_seg = objects.iter().find(|obj| obj.id == seg_id);
2249
2250        if let Some(intersecting_seg) = intersecting_seg {
2251            // Use a larger epsilon for checking if intersection is at another segment's endpoint
2252            let endpoint_epsilon = EPSILON_POINT_ON_SEGMENT * 1000.0; // 0.001mm
2253            let is_other_seg_endpoint = segment_endpoint_points(intersecting_seg, objects, default_unit)
2254                .into_iter()
2255                .any(|(_, endpoint)| {
2256                    let dist_to_endpoint = ((closest_candidate.point.x - endpoint.x).powi(2)
2257                        + (closest_candidate.point.y - endpoint.y).powi(2))
2258                    .sqrt();
2259                    dist_to_endpoint < endpoint_epsilon
2260                });
2261
2262            // If the intersection point is another segment's endpoint (even without coincident constraint),
2263            // return segEndPoint instead of intersection
2264            if is_other_seg_endpoint {
2265                let endpoint = match direction {
2266                    TrimDirection::Left => trim_curve.start,
2267                    TrimDirection::Right => trim_curve.end,
2268                };
2269                return Ok(TrimTermination::SegEndPoint {
2270                    trim_termination_coords: endpoint,
2271                });
2272            }
2273        }
2274
2275        // Also check if intersection is at our arc's endpoint
2276        let endpoint_t = match direction {
2277            TrimDirection::Left => 0.0,
2278            TrimDirection::Right => 1.0,
2279        };
2280        let endpoint = match direction {
2281            TrimDirection::Left => trim_curve.start,
2282            TrimDirection::Right => trim_curve.end,
2283        };
2284        let dist_to_endpoint_param = (closest_candidate.t - endpoint_t).abs();
2285        let dist_to_endpoint_coords = ((closest_candidate.point.x - endpoint.x)
2286            * (closest_candidate.point.x - endpoint.x)
2287            + (closest_candidate.point.y - endpoint.y) * (closest_candidate.point.y - endpoint.y))
2288            .sqrt();
2289
2290        let is_at_endpoint =
2291            dist_to_endpoint_param < EPSILON_POINT_ON_SEGMENT || dist_to_endpoint_coords < EPSILON_POINT_ON_SEGMENT;
2292
2293        if is_at_endpoint {
2294            // Intersection is at our endpoint -> segEndPoint
2295            return Ok(TrimTermination::SegEndPoint {
2296                trim_termination_coords: endpoint,
2297            });
2298        }
2299    }
2300
2301    // Check if the closest candidate is an intersection at an endpoint
2302    let endpoint_t_for_return = match direction {
2303        TrimDirection::Left => 0.0,
2304        TrimDirection::Right => 1.0,
2305    };
2306    if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Intersection {
2307        let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2308        if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2309            // Intersection is at endpoint - check if there's a coincident constraint
2310            // or if it's just a numerical precision issue
2311            let endpoint = match direction {
2312                TrimDirection::Left => trim_curve.start,
2313                TrimDirection::Right => trim_curve.end,
2314            };
2315            return Ok(TrimTermination::SegEndPoint {
2316                trim_termination_coords: endpoint,
2317            });
2318        }
2319    }
2320
2321    // Check if the closest candidate is an endpoint at the trim spawn segment's endpoint
2322    let endpoint = match direction {
2323        TrimDirection::Left => trim_curve.start,
2324        TrimDirection::Right => trim_curve.end,
2325    };
2326    if !is_circle_segment && closest_candidate.candidate_type == CandidateType::Endpoint {
2327        let dist_to_endpoint = (closest_candidate.t - endpoint_t_for_return).abs();
2328        if dist_to_endpoint < EPSILON_POINT_ON_SEGMENT {
2329            // This is our own endpoint, return it
2330            return Ok(TrimTermination::SegEndPoint {
2331                trim_termination_coords: endpoint,
2332            });
2333        }
2334    }
2335
2336    // Return appropriate termination type
2337    if closest_candidate.candidate_type == CandidateType::Coincident {
2338        // Even if at endpoint, return coincident type because it's a constraint-based termination
2339        Ok(TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
2340            trim_termination_coords: closest_candidate.point,
2341            intersecting_seg_id: closest_candidate
2342                .segment_id
2343                .ok_or_else(|| "Missing segment_id for coincident".to_string())?,
2344            other_segment_point_id: closest_candidate
2345                .point_id
2346                .ok_or_else(|| "Missing point_id for coincident".to_string())?,
2347        })
2348    } else if closest_candidate.candidate_type == CandidateType::Intersection {
2349        Ok(TrimTermination::Intersection {
2350            trim_termination_coords: closest_candidate.point,
2351            intersecting_seg_id: closest_candidate
2352                .segment_id
2353                .ok_or_else(|| "Missing segment_id for intersection".to_string())?,
2354        })
2355    } else {
2356        if is_circle_segment {
2357            return Err("Circle trim termination unexpectedly resolved to endpoint".to_string());
2358        }
2359        // endpoint
2360        Ok(TrimTermination::SegEndPoint {
2361            trim_termination_coords: closest_candidate.point,
2362        })
2363    }
2364}
2365
2366/// Execute the core trim loop.
2367/// This function handles the iteration through trim points, finding intersections,
2368/// and determining strategies. It calls the provided callback to execute operations.
2369///
2370/// The callback receives:
2371/// - The strategy (list of operations to execute)
2372/// - The current scene graph delta
2373///
2374/// The callback should return:
2375/// - The updated scene graph delta after executing operations
2376#[cfg(test)]
2377#[allow(dead_code)]
2378pub(crate) async fn execute_trim_loop<F, Fut>(
2379    points: &[Coords2d],
2380    default_unit: UnitLength,
2381    initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2382    mut execute_operations: F,
2383) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>
2384where
2385    F: FnMut(Vec<TrimOperation>, crate::frontend::api::SceneGraphDelta) -> Fut,
2386    Fut: std::future::Future<
2387            Output = Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String>,
2388        >,
2389{
2390    // Trim line points are expected in millimeters and normalized to the current unit here.
2391    let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2392    let points = normalized_points.as_slice();
2393
2394    let mut start_index = 0;
2395    let max_iterations = 1000;
2396    let mut iteration_count = 0;
2397    let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2398        crate::frontend::api::SourceDelta { text: String::new() },
2399        initial_scene_graph_delta.clone(),
2400    ));
2401    let mut invalidates_ids = false;
2402    let mut current_scene_graph_delta = initial_scene_graph_delta;
2403    let circle_delete_fallback_strategy =
2404        |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
2405            if !error.contains("No trim termination candidate found for circle") {
2406                return None;
2407            }
2408            let is_circle = scene_objects
2409                .iter()
2410                .find(|obj| obj.id == segment_id)
2411                .is_some_and(|obj| {
2412                    matches!(
2413                        obj.kind,
2414                        ObjectKind::Segment {
2415                            segment: Segment::Circle(_)
2416                        }
2417                    )
2418                });
2419            if is_circle {
2420                Some(vec![TrimOperation::SimpleTrim {
2421                    segment_to_trim_id: segment_id,
2422                }])
2423            } else {
2424                None
2425            }
2426        };
2427
2428    while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2429        iteration_count += 1;
2430
2431        // Get next trim result
2432        let next_trim_spawn = get_next_trim_spawn(
2433            points,
2434            start_index,
2435            &current_scene_graph_delta.new_graph.objects,
2436            default_unit,
2437        );
2438
2439        match &next_trim_spawn {
2440            TrimItem::None { next_index } => {
2441                let old_start_index = start_index;
2442                start_index = *next_index;
2443
2444                // Fail-safe: if start_index didn't advance, force it to advance
2445                if start_index <= old_start_index {
2446                    start_index = old_start_index + 1;
2447                }
2448
2449                // Early exit if we've reached the end
2450                if start_index >= points.len().saturating_sub(1) {
2451                    break;
2452                }
2453                continue;
2454            }
2455            TrimItem::Spawn {
2456                trim_spawn_seg_id,
2457                trim_spawn_coords,
2458                next_index,
2459                ..
2460            } => {
2461                // Get terminations
2462                let terminations = match get_trim_spawn_terminations(
2463                    *trim_spawn_seg_id,
2464                    points,
2465                    &current_scene_graph_delta.new_graph.objects,
2466                    default_unit,
2467                ) {
2468                    Ok(terms) => terms,
2469                    Err(e) => {
2470                        crate::logln!("Error getting trim spawn terminations: {}", e);
2471                        if let Some(strategy) = circle_delete_fallback_strategy(
2472                            &e,
2473                            *trim_spawn_seg_id,
2474                            &current_scene_graph_delta.new_graph.objects,
2475                        ) {
2476                            match execute_operations(strategy, current_scene_graph_delta.clone()).await {
2477                                Ok((source_delta, scene_graph_delta)) => {
2478                                    last_result = Some((source_delta, scene_graph_delta.clone()));
2479                                    invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2480                                    current_scene_graph_delta = scene_graph_delta;
2481                                }
2482                                Err(exec_err) => {
2483                                    crate::logln!(
2484                                        "Error executing circle-delete fallback trim operation: {}",
2485                                        exec_err
2486                                    );
2487                                }
2488                            }
2489
2490                            let old_start_index = start_index;
2491                            start_index = *next_index;
2492                            if start_index <= old_start_index {
2493                                start_index = old_start_index + 1;
2494                            }
2495                            continue;
2496                        }
2497
2498                        let old_start_index = start_index;
2499                        start_index = *next_index;
2500                        if start_index <= old_start_index {
2501                            start_index = old_start_index + 1;
2502                        }
2503                        continue;
2504                    }
2505                };
2506
2507                // Get trim strategy
2508                let trim_spawn_segment = current_scene_graph_delta
2509                    .new_graph
2510                    .objects
2511                    .iter()
2512                    .find(|obj| obj.id == *trim_spawn_seg_id)
2513                    .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
2514
2515                let plan = match build_trim_plan(
2516                    *trim_spawn_seg_id,
2517                    *trim_spawn_coords,
2518                    trim_spawn_segment,
2519                    &terminations.left_side,
2520                    &terminations.right_side,
2521                    &current_scene_graph_delta.new_graph.objects,
2522                    default_unit,
2523                ) {
2524                    Ok(plan) => plan,
2525                    Err(e) => {
2526                        crate::logln!("Error determining trim strategy: {}", e);
2527                        let old_start_index = start_index;
2528                        start_index = *next_index;
2529                        if start_index <= old_start_index {
2530                            start_index = old_start_index + 1;
2531                        }
2532                        continue;
2533                    }
2534                };
2535                let strategy = lower_trim_plan(&plan);
2536
2537                // Keep processing the same trim polyline segment after geometry-changing ops.
2538                // This allows a single stroke to trim multiple intersected segments.
2539                let geometry_was_modified = trim_plan_modifies_geometry(&plan);
2540
2541                // Execute operations via callback
2542                match execute_operations(strategy, current_scene_graph_delta.clone()).await {
2543                    Ok((source_delta, scene_graph_delta)) => {
2544                        last_result = Some((source_delta, scene_graph_delta.clone()));
2545                        invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2546                        current_scene_graph_delta = scene_graph_delta;
2547                    }
2548                    Err(e) => {
2549                        crate::logln!("Error executing trim operations: {}", e);
2550                        // Continue to next segment
2551                    }
2552                }
2553
2554                // Move to next segment
2555                let old_start_index = start_index;
2556                start_index = *next_index;
2557
2558                // Fail-safe: if start_index didn't advance, force it to advance
2559                if start_index <= old_start_index && !geometry_was_modified {
2560                    start_index = old_start_index + 1;
2561                }
2562            }
2563        }
2564    }
2565
2566    if iteration_count >= max_iterations {
2567        return Err(format!("Reached max iterations ({})", max_iterations));
2568    }
2569
2570    // Return the last result
2571    last_result.ok_or_else(|| "No trim operations were executed".to_string())
2572}
2573
2574/// Result of executing trim flow
2575#[cfg(all(feature = "artifact-graph", test))]
2576#[derive(Debug, Clone)]
2577pub struct TrimFlowResult {
2578    pub kcl_code: String,
2579    pub invalidates_ids: bool,
2580}
2581
2582/// Execute a complete trim flow from KCL code to KCL code.
2583/// This is a high-level function that sets up the frontend state and executes the trim loop.
2584///
2585/// This function:
2586/// 1. Parses the input KCL code
2587/// 2. Sets up ExecutorContext and FrontendState
2588/// 3. Executes the initial code to get the scene graph
2589/// 4. Runs the trim loop using `execute_trim_loop`
2590/// 5. Returns the resulting KCL code
2591///
2592/// This is designed for testing and simple use cases. For more complex scenarios
2593/// (like WASM with batching), use `execute_trim_loop` directly with a custom callback.
2594///
2595/// Note: This function is only available for non-WASM builds (tests) and uses
2596/// a mock executor context so tests can run without an engine token.
2597#[cfg(all(not(target_arch = "wasm32"), feature = "artifact-graph", test))]
2598pub(crate) async fn execute_trim_flow(
2599    kcl_code: &str,
2600    trim_points: &[Coords2d],
2601    sketch_id: ObjectId,
2602) -> Result<TrimFlowResult, String> {
2603    use crate::ExecutorContext;
2604    use crate::Program;
2605    use crate::execution::MockConfig;
2606    use crate::frontend::FrontendState;
2607    use crate::frontend::api::Version;
2608
2609    // Parse KCL code
2610    let parse_result = Program::parse(kcl_code).map_err(|e| format!("Failed to parse KCL: {}", e))?;
2611    let (program_opt, errors) = parse_result;
2612    if !errors.is_empty() {
2613        return Err(format!("Failed to parse KCL: {:?}", errors));
2614    }
2615    let program = program_opt.ok_or_else(|| "No AST produced".to_string())?;
2616
2617    let mock_ctx = ExecutorContext::new_mock(None).await;
2618
2619    // Use a guard to ensure context is closed even on error
2620    let result = async {
2621        let mut frontend = FrontendState::new();
2622
2623        // Set the program
2624        frontend.program = program.clone();
2625
2626        let exec_outcome = mock_ctx
2627            .run_mock(&program, &MockConfig::default())
2628            .await
2629            .map_err(|e| format!("Failed to execute program: {}", e.error.message()))?;
2630
2631        let exec_outcome = frontend.update_state_after_exec(exec_outcome, false);
2632        #[allow(unused_mut)] // mut is needed when artifact-graph feature is enabled
2633        let mut initial_scene_graph = frontend.scene_graph.clone();
2634
2635        // If scene graph is empty, try to get objects from exec_outcome.scene_objects
2636        // (this is only available when artifact-graph feature is enabled)
2637        #[cfg(feature = "artifact-graph")]
2638        if initial_scene_graph.objects.is_empty() && !exec_outcome.scene_objects.is_empty() {
2639            initial_scene_graph.objects = exec_outcome.scene_objects.clone();
2640        }
2641
2642        // Get the sketch ID from the scene graph
2643        // First try sketch_mode, then try to find a sketch object, then fall back to provided sketch_id
2644        let actual_sketch_id = if let Some(sketch_mode) = initial_scene_graph.sketch_mode {
2645            sketch_mode
2646        } else {
2647            // Try to find a sketch object in the scene graph
2648            initial_scene_graph
2649                .objects
2650                .iter()
2651                .find(|obj| matches!(obj.kind, crate::frontend::api::ObjectKind::Sketch { .. }))
2652                .map(|obj| obj.id)
2653                .unwrap_or(sketch_id) // Fall back to provided sketch_id
2654        };
2655
2656        let version = Version(0);
2657        let initial_scene_graph_delta = crate::frontend::api::SceneGraphDelta {
2658            new_graph: initial_scene_graph,
2659            new_objects: vec![],
2660            invalidates_ids: false,
2661            exec_outcome,
2662        };
2663
2664        // Execute the trim loop with a callback that executes operations using SketchApi
2665        // We need to use a different approach since we can't easily capture mutable references in closures
2666        // Instead, we'll use a helper that takes the necessary parameters
2667        // Use mock_ctx for operations (SketchApi methods require mock context)
2668        let (source_delta, scene_graph_delta) = execute_trim_loop_with_context(
2669            trim_points,
2670            initial_scene_graph_delta,
2671            &mut frontend,
2672            &mock_ctx,
2673            version,
2674            actual_sketch_id,
2675        )
2676        .await?;
2677
2678        // Return the source delta text - this should contain the full updated KCL code
2679        // If it's empty, that means no operations were executed, which is an error
2680        if source_delta.text.is_empty() {
2681            return Err("No trim operations were executed - source delta is empty".to_string());
2682        }
2683
2684        Ok(TrimFlowResult {
2685            kcl_code: source_delta.text,
2686            invalidates_ids: scene_graph_delta.invalidates_ids,
2687        })
2688    }
2689    .await;
2690
2691    // Clean up context regardless of success or failure
2692    mock_ctx.close().await;
2693
2694    result
2695}
2696
2697/// Execute the trim loop with a context struct that provides access to FrontendState.
2698/// This is a convenience wrapper that inlines the loop to avoid borrow checker issues with closures.
2699/// The core loop logic is duplicated here, but this allows direct access to frontend and ctx.
2700///
2701/// Trim line points are expected in millimeters and are normalized to the current/default unit.
2702pub async fn execute_trim_loop_with_context(
2703    points: &[Coords2d],
2704    initial_scene_graph_delta: crate::frontend::api::SceneGraphDelta,
2705    frontend: &mut crate::frontend::FrontendState,
2706    ctx: &crate::ExecutorContext,
2707    version: crate::frontend::api::Version,
2708    sketch_id: ObjectId,
2709) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
2710    // Trim line points are expected in millimeters and normalized to the current unit here.
2711    let default_unit = frontend.default_length_unit();
2712    let normalized_points = normalize_trim_points_to_unit(points, default_unit);
2713
2714    // We inline the loop logic here to avoid borrow checker issues with closures capturing mutable references
2715    // This duplicates the loop from execute_trim_loop, but allows us to access frontend and ctx directly
2716    let mut current_scene_graph_delta = initial_scene_graph_delta.clone();
2717    let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = Some((
2718        crate::frontend::api::SourceDelta { text: String::new() },
2719        initial_scene_graph_delta.clone(),
2720    ));
2721    let mut invalidates_ids = false;
2722    let mut start_index = 0;
2723    let max_iterations = 1000;
2724    let mut iteration_count = 0;
2725    let circle_delete_fallback_strategy =
2726        |error: &str, segment_id: ObjectId, scene_objects: &[Object]| -> Option<Vec<TrimOperation>> {
2727            if !error.contains("No trim termination candidate found for circle") {
2728                return None;
2729            }
2730            let is_circle = scene_objects
2731                .iter()
2732                .find(|obj| obj.id == segment_id)
2733                .is_some_and(|obj| {
2734                    matches!(
2735                        obj.kind,
2736                        ObjectKind::Segment {
2737                            segment: Segment::Circle(_)
2738                        }
2739                    )
2740                });
2741            if is_circle {
2742                Some(vec![TrimOperation::SimpleTrim {
2743                    segment_to_trim_id: segment_id,
2744                }])
2745            } else {
2746                None
2747            }
2748        };
2749
2750    let points = normalized_points.as_slice();
2751
2752    while start_index < points.len().saturating_sub(1) && iteration_count < max_iterations {
2753        iteration_count += 1;
2754
2755        // Get next trim result
2756        let next_trim_spawn = get_next_trim_spawn(
2757            points,
2758            start_index,
2759            &current_scene_graph_delta.new_graph.objects,
2760            default_unit,
2761        );
2762
2763        match &next_trim_spawn {
2764            TrimItem::None { next_index } => {
2765                let old_start_index = start_index;
2766                start_index = *next_index;
2767                if start_index <= old_start_index {
2768                    start_index = old_start_index + 1;
2769                }
2770                if start_index >= points.len().saturating_sub(1) {
2771                    break;
2772                }
2773                continue;
2774            }
2775            TrimItem::Spawn {
2776                trim_spawn_seg_id,
2777                trim_spawn_coords,
2778                next_index,
2779                ..
2780            } => {
2781                // Get terminations
2782                let terminations = match get_trim_spawn_terminations(
2783                    *trim_spawn_seg_id,
2784                    points,
2785                    &current_scene_graph_delta.new_graph.objects,
2786                    default_unit,
2787                ) {
2788                    Ok(terms) => terms,
2789                    Err(e) => {
2790                        crate::logln!("Error getting trim spawn terminations: {}", e);
2791                        if let Some(strategy) = circle_delete_fallback_strategy(
2792                            &e,
2793                            *trim_spawn_seg_id,
2794                            &current_scene_graph_delta.new_graph.objects,
2795                        ) {
2796                            match execute_trim_operations_simple(
2797                                strategy.clone(),
2798                                &current_scene_graph_delta,
2799                                frontend,
2800                                ctx,
2801                                version,
2802                                sketch_id,
2803                            )
2804                            .await
2805                            {
2806                                Ok((source_delta, scene_graph_delta)) => {
2807                                    invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2808                                    last_result = Some((source_delta, scene_graph_delta.clone()));
2809                                    current_scene_graph_delta = scene_graph_delta;
2810                                }
2811                                Err(exec_err) => {
2812                                    crate::logln!(
2813                                        "Error executing circle-delete fallback trim operation: {}",
2814                                        exec_err
2815                                    );
2816                                }
2817                            }
2818
2819                            let old_start_index = start_index;
2820                            start_index = *next_index;
2821                            if start_index <= old_start_index {
2822                                start_index = old_start_index + 1;
2823                            }
2824                            continue;
2825                        }
2826
2827                        let old_start_index = start_index;
2828                        start_index = *next_index;
2829                        if start_index <= old_start_index {
2830                            start_index = old_start_index + 1;
2831                        }
2832                        continue;
2833                    }
2834                };
2835
2836                // Get trim strategy
2837                let trim_spawn_segment = current_scene_graph_delta
2838                    .new_graph
2839                    .objects
2840                    .iter()
2841                    .find(|obj| obj.id == *trim_spawn_seg_id)
2842                    .ok_or_else(|| format!("Trim spawn segment {} not found", trim_spawn_seg_id.0))?;
2843
2844                let plan = match build_trim_plan(
2845                    *trim_spawn_seg_id,
2846                    *trim_spawn_coords,
2847                    trim_spawn_segment,
2848                    &terminations.left_side,
2849                    &terminations.right_side,
2850                    &current_scene_graph_delta.new_graph.objects,
2851                    default_unit,
2852                ) {
2853                    Ok(plan) => plan,
2854                    Err(e) => {
2855                        crate::logln!("Error determining trim strategy: {}", e);
2856                        let old_start_index = start_index;
2857                        start_index = *next_index;
2858                        if start_index <= old_start_index {
2859                            start_index = old_start_index + 1;
2860                        }
2861                        continue;
2862                    }
2863                };
2864                let strategy = lower_trim_plan(&plan);
2865
2866                // Keep processing the same trim polyline segment after geometry-changing ops.
2867                // This allows a single stroke to trim multiple intersected segments.
2868                let geometry_was_modified = trim_plan_modifies_geometry(&plan);
2869
2870                // Execute operations
2871                match execute_trim_operations_simple(
2872                    strategy.clone(),
2873                    &current_scene_graph_delta,
2874                    frontend,
2875                    ctx,
2876                    version,
2877                    sketch_id,
2878                )
2879                .await
2880                {
2881                    Ok((source_delta, scene_graph_delta)) => {
2882                        invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
2883                        last_result = Some((source_delta, scene_graph_delta.clone()));
2884                        current_scene_graph_delta = scene_graph_delta;
2885                    }
2886                    Err(e) => {
2887                        crate::logln!("Error executing trim operations: {}", e);
2888                    }
2889                }
2890
2891                // Move to next segment
2892                let old_start_index = start_index;
2893                start_index = *next_index;
2894                if start_index <= old_start_index && !geometry_was_modified {
2895                    start_index = old_start_index + 1;
2896                }
2897            }
2898        }
2899    }
2900
2901    if iteration_count >= max_iterations {
2902        return Err(format!("Reached max iterations ({})", max_iterations));
2903    }
2904
2905    let (source_delta, mut scene_graph_delta) =
2906        last_result.ok_or_else(|| "No trim operations were executed".to_string())?;
2907    // Set invalidates_ids if any operation invalidated IDs
2908    scene_graph_delta.invalidates_ids = invalidates_ids;
2909    Ok((source_delta, scene_graph_delta))
2910}
2911
2912/// Determine the trim strategy based on the terminations found on both sides
2913///
2914/// Once we have the termination of both sides, we have all the information we need to come up with a trim strategy.
2915/// In the below x is the trim spawn.
2916///
2917/// ## When both sides are the end of a segment
2918///
2919/// ```
2920/// o - -----x - -----o
2921/// ```
2922///
2923/// This is the simplest and we just delete the segment. This includes when the ends of the segment have
2924/// coincident constraints, as the delete API cascade deletes these constraints
2925///
2926/// ## When one side is the end of the segment and the other side is either an intersection or has another segment endpoint coincident with it
2927///
2928/// ```
2929///        /
2930/// -------/---x--o
2931///      /
2932/// ```
2933/// OR
2934/// ```
2935/// ----o---x---o
2936///    /
2937///   /
2938/// ```
2939///
2940/// In both of these cases, we need to edit one end of the segment to be the location of the
2941/// intersection/coincident point of this other segment though:
2942/// - If it's an intersection, we need to create a point-segment coincident constraint
2943/// ```
2944///        /
2945/// -------o
2946///      /
2947/// ```
2948/// - If it's a coincident endpoint, we need to create a point-point coincident constraint
2949///
2950/// ```
2951/// ----o
2952///    /
2953///   /
2954/// ```
2955///
2956/// ## When both sides are either intersections or coincident endpoints
2957///
2958/// ```
2959///        /
2960/// -------/---x----o------
2961///      /         |
2962/// ```
2963///
2964/// We need to split the segment in two, which basically means editing the existing segment to be one side
2965/// of the split, and adding a new segment for the other side of the split. And then there is lots of
2966/// complications around how to migrate constraints applied to each side of the segment, to list a couple
2967/// of considerations:
2968/// - Coincident constraints on either side need to be migrated to the correct side
2969/// - Angle based constraints (parallel, perpendicular, horizontal, vertical), need to be applied to both sides of the trim
2970/// - If the segment getting split is an arc, and there's a constraints applied to an arc's center, this should be applied to both arcs after they are split.
2971pub(crate) fn build_trim_plan(
2972    trim_spawn_id: ObjectId,
2973    trim_spawn_coords: Coords2d,
2974    trim_spawn_segment: &Object,
2975    left_side: &TrimTermination,
2976    right_side: &TrimTermination,
2977    objects: &[Object],
2978    default_unit: UnitLength,
2979) -> Result<TrimPlan, String> {
2980    // Simple trim: both sides are endpoints
2981    if matches!(left_side, TrimTermination::SegEndPoint { .. })
2982        && matches!(right_side, TrimTermination::SegEndPoint { .. })
2983    {
2984        return Ok(TrimPlan::DeleteSegment {
2985            segment_id: trim_spawn_id,
2986        });
2987    }
2988
2989    // Helper to check if a side is an intersection or coincident
2990    let is_intersect_or_coincident = |side: &TrimTermination| -> bool {
2991        matches!(
2992            side,
2993            TrimTermination::Intersection { .. }
2994                | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
2995        )
2996    };
2997
2998    let left_side_needs_tail_cut = is_intersect_or_coincident(left_side) && !is_intersect_or_coincident(right_side);
2999    let right_side_needs_tail_cut = is_intersect_or_coincident(right_side) && !is_intersect_or_coincident(left_side);
3000
3001    // Validate trim spawn segment using native types
3002    let ObjectKind::Segment { segment } = &trim_spawn_segment.kind else {
3003        return Err("Trim spawn segment is not a segment".to_string());
3004    };
3005
3006    let (_segment_type, ctor) = match segment {
3007        Segment::Line(line) => ("Line", &line.ctor),
3008        Segment::Arc(arc) => ("Arc", &arc.ctor),
3009        Segment::Circle(circle) => ("Circle", &circle.ctor),
3010        _ => {
3011            return Err("Trim spawn segment is not a Line, Arc, or Circle".to_string());
3012        }
3013    };
3014
3015    // Extract units from the existing ctor's start point
3016    let units = match ctor {
3017        SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
3018            crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
3019            _ => NumericSuffix::Mm,
3020        },
3021        SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
3022            crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
3023            _ => NumericSuffix::Mm,
3024        },
3025        SegmentCtor::Circle(circle_ctor) => match &circle_ctor.start.x {
3026            crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
3027            _ => NumericSuffix::Mm,
3028        },
3029        _ => NumericSuffix::Mm,
3030    };
3031
3032    // Helper to find distance constraints that reference a segment (via owned points)
3033    let find_distance_constraints_for_segment = |segment_id: ObjectId| -> Vec<ObjectId> {
3034        let mut constraint_ids = Vec::new();
3035        for obj in objects {
3036            let ObjectKind::Constraint { constraint } = &obj.kind else {
3037                continue;
3038            };
3039
3040            let Constraint::Distance(distance) = constraint else {
3041                continue;
3042            };
3043
3044            // Only delete distance constraints where BOTH points are owned by this segment.
3045            // Distance constraints that reference points on other segments should be preserved,
3046            // as they define relationships between this segment and other geometry that remain valid
3047            // even when this segment is trimmed. Only constraints that measure distances between
3048            // points on the same segment (e.g., segment length constraints) should be deleted.
3049            let points_owned_by_segment: Vec<bool> = distance
3050                .point_ids()
3051                .map(|point_id| {
3052                    if let Some(point_obj) = objects.iter().find(|o| o.id == point_id)
3053                        && let ObjectKind::Segment { segment } = &point_obj.kind
3054                        && let Segment::Point(point) = segment
3055                        && let Some(owner_id) = point.owner
3056                    {
3057                        return owner_id == segment_id;
3058                    }
3059                    false
3060                })
3061                .collect();
3062
3063            // Only include if ALL points are owned by this segment
3064            if points_owned_by_segment.len() == 2 && points_owned_by_segment.iter().all(|&owned| owned) {
3065                constraint_ids.push(obj.id);
3066            }
3067        }
3068        constraint_ids
3069    };
3070
3071    // Helper to find existing point-segment coincident constraint (using native types)
3072    let find_existing_point_segment_coincident =
3073        |trim_seg_id: ObjectId, intersecting_seg_id: ObjectId| -> CoincidentData {
3074            // If the intersecting id itself is a point, try a fast lookup using it directly
3075            let lookup_by_point_id = |point_id: ObjectId| -> Option<CoincidentData> {
3076                for obj in objects {
3077                    let ObjectKind::Constraint { constraint } = &obj.kind else {
3078                        continue;
3079                    };
3080
3081                    let Constraint::Coincident(coincident) = constraint else {
3082                        continue;
3083                    };
3084
3085                    let involves_trim_seg = coincident.segment_ids().any(|id| id == trim_seg_id || id == point_id);
3086                    let involves_point = coincident.contains_segment(point_id);
3087
3088                    if involves_trim_seg && involves_point {
3089                        return Some(CoincidentData {
3090                            intersecting_seg_id,
3091                            intersecting_endpoint_point_id: Some(point_id),
3092                            existing_point_segment_constraint_id: Some(obj.id),
3093                        });
3094                    }
3095                }
3096                None
3097            };
3098
3099            // Collect trim endpoints using native types
3100            let trim_seg = objects.iter().find(|obj| obj.id == trim_seg_id);
3101
3102            let mut trim_endpoint_ids: Vec<ObjectId> = Vec::new();
3103            if let Some(seg) = trim_seg
3104                && let ObjectKind::Segment { segment } = &seg.kind
3105            {
3106                match segment {
3107                    Segment::Line(line) => {
3108                        trim_endpoint_ids.push(line.start);
3109                        trim_endpoint_ids.push(line.end);
3110                    }
3111                    Segment::Arc(arc) => {
3112                        trim_endpoint_ids.push(arc.start);
3113                        trim_endpoint_ids.push(arc.end);
3114                    }
3115                    _ => {}
3116                }
3117            }
3118
3119            let intersecting_obj = objects.iter().find(|obj| obj.id == intersecting_seg_id);
3120
3121            if let Some(obj) = intersecting_obj
3122                && let ObjectKind::Segment { segment } = &obj.kind
3123                && let Segment::Point(_) = segment
3124                && let Some(found) = lookup_by_point_id(intersecting_seg_id)
3125            {
3126                return found;
3127            }
3128
3129            // Collect intersecting endpoint IDs using native types
3130            let mut intersecting_endpoint_ids: Vec<ObjectId> = Vec::new();
3131            if let Some(obj) = intersecting_obj
3132                && let ObjectKind::Segment { segment } = &obj.kind
3133            {
3134                match segment {
3135                    Segment::Line(line) => {
3136                        intersecting_endpoint_ids.push(line.start);
3137                        intersecting_endpoint_ids.push(line.end);
3138                    }
3139                    Segment::Arc(arc) => {
3140                        intersecting_endpoint_ids.push(arc.start);
3141                        intersecting_endpoint_ids.push(arc.end);
3142                    }
3143                    _ => {}
3144                }
3145            }
3146
3147            // Also include the intersecting_seg_id itself (it might already be a point id)
3148            intersecting_endpoint_ids.push(intersecting_seg_id);
3149
3150            // Search for constraints involving trim segment (or trim endpoints) and intersecting endpoints/points
3151            for obj in objects {
3152                let ObjectKind::Constraint { constraint } = &obj.kind else {
3153                    continue;
3154                };
3155
3156                let Constraint::Coincident(coincident) = constraint else {
3157                    continue;
3158                };
3159
3160                let constraint_segment_ids: Vec<ObjectId> = coincident.get_segments();
3161
3162                // Check if constraint involves the trim segment itself OR any trim endpoint
3163                let involves_trim_seg = constraint_segment_ids.contains(&trim_seg_id)
3164                    || trim_endpoint_ids.iter().any(|&id| constraint_segment_ids.contains(&id));
3165
3166                if !involves_trim_seg {
3167                    continue;
3168                }
3169
3170                // Check if any intersecting endpoint/point is involved
3171                if let Some(&intersecting_endpoint_id) = intersecting_endpoint_ids
3172                    .iter()
3173                    .find(|&&id| constraint_segment_ids.contains(&id))
3174                {
3175                    return CoincidentData {
3176                        intersecting_seg_id,
3177                        intersecting_endpoint_point_id: Some(intersecting_endpoint_id),
3178                        existing_point_segment_constraint_id: Some(obj.id),
3179                    };
3180                }
3181            }
3182
3183            // No existing constraint found
3184            CoincidentData {
3185                intersecting_seg_id,
3186                intersecting_endpoint_point_id: None,
3187                existing_point_segment_constraint_id: None,
3188            }
3189        };
3190
3191    // Helper to find point-segment coincident constraints on an endpoint (using native types)
3192    let find_point_segment_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<serde_json::Value> {
3193        let mut constraints: Vec<serde_json::Value> = Vec::new();
3194        for obj in objects {
3195            let ObjectKind::Constraint { constraint } = &obj.kind else {
3196                continue;
3197            };
3198
3199            let Constraint::Coincident(coincident) = constraint else {
3200                continue;
3201            };
3202
3203            // Check if this constraint involves the endpoint
3204            if !coincident.contains_segment(endpoint_point_id) {
3205                continue;
3206            }
3207
3208            // Find the other entity
3209            let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
3210
3211            if let Some(other_id) = other_segment_id
3212                && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
3213            {
3214                // Check if other is a segment (not a point)
3215                if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
3216                    constraints.push(serde_json::json!({
3217                        "constraintId": obj.id.0,
3218                        "segmentOrPointId": other_id.0,
3219                    }));
3220                }
3221            }
3222        }
3223        constraints
3224    };
3225
3226    // Helper to find point-point coincident constraints on an endpoint (using native types)
3227    // Returns constraint IDs
3228    let find_point_point_coincident_constraints = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
3229        let mut constraint_ids = Vec::new();
3230        for obj in objects {
3231            let ObjectKind::Constraint { constraint } = &obj.kind else {
3232                continue;
3233            };
3234
3235            let Constraint::Coincident(coincident) = constraint else {
3236                continue;
3237            };
3238
3239            // Check if this constraint involves the endpoint
3240            if !coincident.contains_segment(endpoint_point_id) {
3241                continue;
3242            }
3243
3244            // Check if this is a point-point constraint (all segments are points)
3245            let is_point_point = coincident.segment_ids().all(|seg_id| {
3246                if let Some(seg_obj) = objects.iter().find(|o| o.id == seg_id) {
3247                    matches!(&seg_obj.kind, ObjectKind::Segment { segment } if matches!(segment, Segment::Point(_)))
3248                } else {
3249                    false
3250                }
3251            });
3252
3253            if is_point_point {
3254                constraint_ids.push(obj.id);
3255            }
3256        }
3257        constraint_ids
3258    };
3259
3260    // Helper to find point-segment coincident constraints on an endpoint (using native types)
3261    // Returns constraint IDs
3262    let find_point_segment_coincident_constraint_ids = |endpoint_point_id: ObjectId| -> Vec<ObjectId> {
3263        let mut constraint_ids = Vec::new();
3264        for obj in objects {
3265            let ObjectKind::Constraint { constraint } = &obj.kind else {
3266                continue;
3267            };
3268
3269            let Constraint::Coincident(coincident) = constraint else {
3270                continue;
3271            };
3272
3273            // Check if this constraint involves the endpoint
3274            if !coincident.contains_segment(endpoint_point_id) {
3275                continue;
3276            }
3277
3278            // Find the other entity
3279            let other_segment_id = coincident.segment_ids().find(|&seg_id| seg_id != endpoint_point_id);
3280
3281            if let Some(other_id) = other_segment_id
3282                && let Some(other_obj) = objects.iter().find(|o| o.id == other_id)
3283            {
3284                // Check if other is a segment (not a point) - this is a point-segment constraint
3285                if matches!(&other_obj.kind, ObjectKind::Segment { segment } if !matches!(segment, Segment::Point(_))) {
3286                    constraint_ids.push(obj.id);
3287                }
3288            }
3289        }
3290        constraint_ids
3291    };
3292
3293    let find_midpoint_constraints_for_segment = |segment_id: ObjectId| -> Vec<ObjectId> {
3294        objects
3295            .iter()
3296            .filter_map(|obj| {
3297                let ObjectKind::Constraint { constraint } = &obj.kind else {
3298                    return None;
3299                };
3300
3301                let Constraint::Midpoint(midpoint) = constraint else {
3302                    return None;
3303                };
3304
3305                (midpoint.segment == segment_id).then_some(obj.id)
3306            })
3307            .collect()
3308    };
3309
3310    // Cut tail: one side intersects, one is endpoint
3311    if left_side_needs_tail_cut || right_side_needs_tail_cut {
3312        let side = if left_side_needs_tail_cut {
3313            left_side
3314        } else {
3315            right_side
3316        };
3317
3318        let intersection_coords = match side {
3319            TrimTermination::Intersection {
3320                trim_termination_coords,
3321                ..
3322            }
3323            | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3324                trim_termination_coords,
3325                ..
3326            } => *trim_termination_coords,
3327            TrimTermination::SegEndPoint { .. } => {
3328                return Err("Logic error: side should not be segEndPoint here".to_string());
3329            }
3330        };
3331
3332        let endpoint_to_change = if left_side_needs_tail_cut {
3333            EndpointChanged::End
3334        } else {
3335            EndpointChanged::Start
3336        };
3337
3338        let intersecting_seg_id = match side {
3339            TrimTermination::Intersection {
3340                intersecting_seg_id, ..
3341            }
3342            | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3343                intersecting_seg_id, ..
3344            } => *intersecting_seg_id,
3345            TrimTermination::SegEndPoint { .. } => {
3346                return Err("Logic error".to_string());
3347            }
3348        };
3349
3350        let mut coincident_data = if matches!(
3351            side,
3352            TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3353        ) {
3354            let point_id = match side {
3355                TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3356                    other_segment_point_id, ..
3357                } => *other_segment_point_id,
3358                _ => return Err("Logic error".to_string()),
3359            };
3360            let mut data = find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id);
3361            data.intersecting_endpoint_point_id = Some(point_id);
3362            data
3363        } else {
3364            find_existing_point_segment_coincident(trim_spawn_id, intersecting_seg_id)
3365        };
3366
3367        if matches!(side, TrimTermination::Intersection { .. })
3368            && let Some(point_id) = coincident_data.intersecting_endpoint_point_id
3369        {
3370            let endpoint_is_at_intersection = get_point_coords_from_native(objects, point_id, default_unit)
3371                .is_some_and(|point_coords| {
3372                    ((point_coords.x - intersection_coords.x).powi(2)
3373                        + (point_coords.y - intersection_coords.y).powi(2))
3374                    .sqrt()
3375                        <= EPSILON_POINT_ON_SEGMENT * 1000.0
3376                });
3377
3378            if !endpoint_is_at_intersection {
3379                coincident_data.existing_point_segment_constraint_id = None;
3380                coincident_data.intersecting_endpoint_point_id = None;
3381            }
3382        }
3383
3384        // Find the endpoint that will be trimmed using native types
3385        let trim_seg = objects.iter().find(|obj| obj.id == trim_spawn_id);
3386
3387        let endpoint_point_id = if let Some(seg) = trim_seg {
3388            let ObjectKind::Segment { segment } = &seg.kind else {
3389                return Err("Trim spawn segment is not a segment".to_string());
3390            };
3391            match segment {
3392                Segment::Line(line) => {
3393                    if endpoint_to_change == EndpointChanged::Start {
3394                        Some(line.start)
3395                    } else {
3396                        Some(line.end)
3397                    }
3398                }
3399                Segment::Arc(arc) => {
3400                    if endpoint_to_change == EndpointChanged::Start {
3401                        Some(arc.start)
3402                    } else {
3403                        Some(arc.end)
3404                    }
3405                }
3406                _ => None,
3407            }
3408        } else {
3409            None
3410        };
3411
3412        if let (Some(endpoint_id), Some(existing_constraint_id)) =
3413            (endpoint_point_id, coincident_data.existing_point_segment_constraint_id)
3414        {
3415            let constraint_involves_trimmed_endpoint = objects
3416                .iter()
3417                .find(|obj| obj.id == existing_constraint_id)
3418                .and_then(|obj| match &obj.kind {
3419                    ObjectKind::Constraint {
3420                        constraint: Constraint::Coincident(coincident),
3421                    } => Some(coincident.contains_segment(endpoint_id) || coincident.contains_segment(trim_spawn_id)),
3422                    _ => None,
3423                })
3424                .unwrap_or(false);
3425
3426            if !constraint_involves_trimmed_endpoint {
3427                coincident_data.existing_point_segment_constraint_id = None;
3428                coincident_data.intersecting_endpoint_point_id = None;
3429            }
3430        }
3431
3432        // Find point-point and point-segment constraints to delete
3433        let coincident_end_constraint_to_delete_ids = if let Some(point_id) = endpoint_point_id {
3434            let mut constraint_ids = find_point_point_coincident_constraints(point_id);
3435            // Also find point-segment constraints where the point is the endpoint being trimmed
3436            constraint_ids.extend(find_point_segment_coincident_constraint_ids(point_id));
3437            constraint_ids
3438        } else {
3439            Vec::new()
3440        };
3441
3442        let point_axis_constraint_ids_to_delete = if let Some(point_id) = endpoint_point_id {
3443            objects
3444                .iter()
3445                .filter_map(|obj| {
3446                    let ObjectKind::Constraint { constraint } = &obj.kind else {
3447                        return None;
3448                    };
3449
3450                    point_axis_constraint_references_point(constraint, point_id).then_some(obj.id)
3451                })
3452                .collect::<Vec<_>>()
3453        } else {
3454            Vec::new()
3455        };
3456
3457        // Edit the segment - create new ctor with updated endpoint
3458        let new_ctor = match ctor {
3459            SegmentCtor::Line(line_ctor) => {
3460                // Convert to segment units only; rounding happens at final conversion to output if needed.
3461                let new_point = crate::frontend::sketch::Point2d {
3462                    x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
3463                    y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
3464                };
3465                if endpoint_to_change == EndpointChanged::Start {
3466                    SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3467                        start: new_point,
3468                        end: line_ctor.end.clone(),
3469                        construction: line_ctor.construction,
3470                    })
3471                } else {
3472                    SegmentCtor::Line(crate::frontend::sketch::LineCtor {
3473                        start: line_ctor.start.clone(),
3474                        end: new_point,
3475                        construction: line_ctor.construction,
3476                    })
3477                }
3478            }
3479            SegmentCtor::Arc(arc_ctor) => {
3480                // Convert to segment units only; rounding happens at final conversion to output if needed.
3481                let new_point = crate::frontend::sketch::Point2d {
3482                    x: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.x, default_unit, units)),
3483                    y: crate::frontend::api::Expr::Var(unit_to_number(intersection_coords.y, default_unit, units)),
3484                };
3485                if endpoint_to_change == EndpointChanged::Start {
3486                    SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3487                        start: new_point,
3488                        end: arc_ctor.end.clone(),
3489                        center: arc_ctor.center.clone(),
3490                        construction: arc_ctor.construction,
3491                    })
3492                } else {
3493                    SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
3494                        start: arc_ctor.start.clone(),
3495                        end: new_point,
3496                        center: arc_ctor.center.clone(),
3497                        construction: arc_ctor.construction,
3498                    })
3499                }
3500            }
3501            _ => {
3502                return Err("Unsupported segment type for edit".to_string());
3503            }
3504        };
3505
3506        // Delete old constraints
3507        let mut all_constraint_ids_to_delete: Vec<ObjectId> = Vec::new();
3508        if let Some(constraint_id) = coincident_data.existing_point_segment_constraint_id {
3509            all_constraint_ids_to_delete.push(constraint_id);
3510        }
3511        all_constraint_ids_to_delete.extend(coincident_end_constraint_to_delete_ids);
3512        all_constraint_ids_to_delete.extend(point_axis_constraint_ids_to_delete);
3513        all_constraint_ids_to_delete.extend(find_midpoint_constraints_for_segment(trim_spawn_id));
3514
3515        // Delete distance constraints that reference this segment
3516        // When trimming an endpoint, the distance constraint no longer makes sense
3517        let distance_constraint_ids = find_distance_constraints_for_segment(trim_spawn_id);
3518        all_constraint_ids_to_delete.extend(distance_constraint_ids);
3519
3520        let coincident_target_id = coincident_data
3521            .intersecting_endpoint_point_id
3522            .unwrap_or(intersecting_seg_id);
3523        let adds_curved_segment_coincident = endpoint_point_id
3524            .is_some_and(|point_id| segment_id_is_or_is_owned_by_curve(objects, point_id))
3525            || segment_id_is_or_is_owned_by_curve(objects, coincident_target_id);
3526        let has_midpoint_deletions = all_constraint_ids_to_delete.iter().any(|constraint_id| {
3527            objects
3528                .iter()
3529                .find(|obj| obj.id == *constraint_id)
3530                .is_some_and(|object| {
3531                    matches!(
3532                        object.kind,
3533                        ObjectKind::Constraint {
3534                            constraint: Constraint::Midpoint(_)
3535                        }
3536                    )
3537                })
3538        });
3539
3540        let mut additional_edited_segment_ids = IndexSet::new();
3541        if has_midpoint_deletions || (adds_curved_segment_coincident && all_constraint_ids_to_delete.is_empty()) {
3542            additional_edited_segment_ids.extend(sketch_segment_ids_for_segment(objects, trim_spawn_id));
3543        }
3544
3545        if adds_curved_segment_coincident {
3546            for constraint_id in &all_constraint_ids_to_delete {
3547                let Some(constraint_object) = objects.iter().find(|obj| obj.id == *constraint_id) else {
3548                    continue;
3549                };
3550                let ObjectKind::Constraint {
3551                    constraint: Constraint::Coincident(coincident),
3552                } = &constraint_object.kind
3553                else {
3554                    continue;
3555                };
3556
3557                additional_edited_segment_ids.extend(
3558                    coincident
3559                        .segment_ids()
3560                        .map(|segment_id| owner_or_segment_id(objects, segment_id)),
3561                );
3562            }
3563        }
3564
3565        return Ok(TrimPlan::TailCut {
3566            segment_id: trim_spawn_id,
3567            endpoint_changed: endpoint_to_change,
3568            ctor: new_ctor,
3569            segment_or_point_to_make_coincident_to: intersecting_seg_id,
3570            intersecting_endpoint_point_id: coincident_data.intersecting_endpoint_point_id,
3571            constraint_ids_to_delete: all_constraint_ids_to_delete,
3572            additional_edited_segment_ids: additional_edited_segment_ids.into_iter().collect(),
3573        });
3574    }
3575
3576    // Circle trim: both sides must terminate on intersections/coincident points.
3577    // A circle cannot be "split" into two circles; it is converted into a single arc.
3578    if matches!(segment, Segment::Circle(_)) {
3579        let left_side_intersects = is_intersect_or_coincident(left_side);
3580        let right_side_intersects = is_intersect_or_coincident(right_side);
3581        if !(left_side_intersects && right_side_intersects) {
3582            return Err(format!(
3583                "Unsupported circle trim termination combination: left={:?} right={:?}",
3584                left_side, right_side
3585            ));
3586        }
3587
3588        let left_trim_coords = match left_side {
3589            TrimTermination::SegEndPoint {
3590                trim_termination_coords,
3591            }
3592            | TrimTermination::Intersection {
3593                trim_termination_coords,
3594                ..
3595            }
3596            | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3597                trim_termination_coords,
3598                ..
3599            } => *trim_termination_coords,
3600        };
3601        let right_trim_coords = match right_side {
3602            TrimTermination::SegEndPoint {
3603                trim_termination_coords,
3604            }
3605            | TrimTermination::Intersection {
3606                trim_termination_coords,
3607                ..
3608            }
3609            | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3610                trim_termination_coords,
3611                ..
3612            } => *trim_termination_coords,
3613        };
3614
3615        // If both sides resolve to essentially the same trim point (e.g., tangent-only hit),
3616        // deleting the circle matches expected trim behavior better than creating a zero-length arc.
3617        let trim_points_coincident = ((left_trim_coords.x - right_trim_coords.x)
3618            * (left_trim_coords.x - right_trim_coords.x)
3619            + (left_trim_coords.y - right_trim_coords.y) * (left_trim_coords.y - right_trim_coords.y))
3620            .sqrt()
3621            <= EPSILON_POINT_ON_SEGMENT * 10.0;
3622        if trim_points_coincident {
3623            return Ok(TrimPlan::DeleteSegment {
3624                segment_id: trim_spawn_id,
3625            });
3626        }
3627
3628        let circle_center_coords =
3629            get_position_coords_from_circle(trim_spawn_segment, CirclePoint::Center, objects, default_unit)
3630                .ok_or_else(|| {
3631                    format!(
3632                        "Could not get center coordinates for circle segment {}",
3633                        trim_spawn_id.0
3634                    )
3635                })?;
3636
3637        // The trim removes the side containing the trim spawn. Keep the opposite side.
3638        let spawn_on_left_to_right = is_point_on_arc(
3639            trim_spawn_coords,
3640            circle_center_coords,
3641            left_trim_coords,
3642            right_trim_coords,
3643            EPSILON_POINT_ON_SEGMENT,
3644        );
3645        let (arc_start_coords, arc_end_coords, arc_start_termination, arc_end_termination) = if spawn_on_left_to_right {
3646            (
3647                right_trim_coords,
3648                left_trim_coords,
3649                Box::new(right_side.clone()),
3650                Box::new(left_side.clone()),
3651            )
3652        } else {
3653            (
3654                left_trim_coords,
3655                right_trim_coords,
3656                Box::new(left_side.clone()),
3657                Box::new(right_side.clone()),
3658            )
3659        };
3660
3661        return Ok(TrimPlan::ReplaceCircleWithArc {
3662            circle_id: trim_spawn_id,
3663            arc_start_coords,
3664            arc_end_coords,
3665            arc_start_termination,
3666            arc_end_termination,
3667        });
3668    }
3669
3670    // Split segment: both sides intersect
3671    let left_side_intersects = is_intersect_or_coincident(left_side);
3672    let right_side_intersects = is_intersect_or_coincident(right_side);
3673
3674    if left_side_intersects && right_side_intersects {
3675        // This is the most complex case - split segment
3676        // Get coincident data for both sides
3677        let left_intersecting_seg_id = match left_side {
3678            TrimTermination::Intersection {
3679                intersecting_seg_id, ..
3680            }
3681            | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3682                intersecting_seg_id, ..
3683            } => *intersecting_seg_id,
3684            TrimTermination::SegEndPoint { .. } => {
3685                return Err("Logic error: left side should not be segEndPoint".to_string());
3686            }
3687        };
3688
3689        let right_intersecting_seg_id = match right_side {
3690            TrimTermination::Intersection {
3691                intersecting_seg_id, ..
3692            }
3693            | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3694                intersecting_seg_id, ..
3695            } => *intersecting_seg_id,
3696            TrimTermination::SegEndPoint { .. } => {
3697                return Err("Logic error: right side should not be segEndPoint".to_string());
3698            }
3699        };
3700
3701        let left_coincident_data = if matches!(
3702            left_side,
3703            TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3704        ) {
3705            let point_id = match left_side {
3706                TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3707                    other_segment_point_id, ..
3708                } => *other_segment_point_id,
3709                _ => return Err("Logic error".to_string()),
3710            };
3711            let mut data = find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id);
3712            data.intersecting_endpoint_point_id = Some(point_id);
3713            data
3714        } else {
3715            find_existing_point_segment_coincident(trim_spawn_id, left_intersecting_seg_id)
3716        };
3717
3718        let right_coincident_data = if matches!(
3719            right_side,
3720            TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint { .. }
3721        ) {
3722            let point_id = match right_side {
3723                TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3724                    other_segment_point_id, ..
3725                } => *other_segment_point_id,
3726                _ => return Err("Logic error".to_string()),
3727            };
3728            let mut data = find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id);
3729            data.intersecting_endpoint_point_id = Some(point_id);
3730            data
3731        } else {
3732            find_existing_point_segment_coincident(trim_spawn_id, right_intersecting_seg_id)
3733        };
3734
3735        // Find the endpoints of the segment being split using native types
3736        let (original_start_point_id, original_end_point_id) = match segment {
3737            Segment::Line(line) => (Some(line.start), Some(line.end)),
3738            Segment::Arc(arc) => (Some(arc.start), Some(arc.end)),
3739            _ => (None, None),
3740        };
3741
3742        // Get the original end point coordinates before editing using native types
3743        let original_end_point_coords = match segment {
3744            Segment::Line(_) => {
3745                get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
3746            }
3747            Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
3748            _ => None,
3749        };
3750
3751        let Some(original_end_coords) = original_end_point_coords else {
3752            return Err(
3753                "Could not get original end point coordinates before editing - this is required for split trim"
3754                    .to_string(),
3755            );
3756        };
3757
3758        // Calculate trim coordinates for both sides
3759        let left_trim_coords = match left_side {
3760            TrimTermination::SegEndPoint {
3761                trim_termination_coords,
3762            }
3763            | TrimTermination::Intersection {
3764                trim_termination_coords,
3765                ..
3766            }
3767            | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3768                trim_termination_coords,
3769                ..
3770            } => *trim_termination_coords,
3771        };
3772
3773        let right_trim_coords = match right_side {
3774            TrimTermination::SegEndPoint {
3775                trim_termination_coords,
3776            }
3777            | TrimTermination::Intersection {
3778                trim_termination_coords,
3779                ..
3780            }
3781            | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
3782                trim_termination_coords,
3783                ..
3784            } => *trim_termination_coords,
3785        };
3786
3787        // Check if the split point is at the original end point
3788        let dist_to_original_end = ((right_trim_coords.x - original_end_coords.x)
3789            * (right_trim_coords.x - original_end_coords.x)
3790            + (right_trim_coords.y - original_end_coords.y) * (right_trim_coords.y - original_end_coords.y))
3791            .sqrt();
3792        if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3793            return Err(
3794                "Split point is at original end point - this should be handled as cutTail, not split".to_string(),
3795            );
3796        }
3797
3798        // For now, implement a simplified version that creates the split operation
3799        // The full constraint migration logic is very complex and can be refined during testing
3800        let mut constraints_to_migrate: Vec<ConstraintToMigrate> = Vec::new();
3801        let mut constraints_to_delete_set: IndexSet<ObjectId> = IndexSet::new();
3802
3803        // Add existing point-segment constraints from terminations to delete list
3804        if let Some(constraint_id) = left_coincident_data.existing_point_segment_constraint_id {
3805            constraints_to_delete_set.insert(constraint_id);
3806        }
3807        if let Some(constraint_id) = right_coincident_data.existing_point_segment_constraint_id {
3808            constraints_to_delete_set.insert(constraint_id);
3809        }
3810
3811        if let Some(end_id) = original_end_point_id {
3812            for obj in objects {
3813                let ObjectKind::Constraint { constraint } = &obj.kind else {
3814                    continue;
3815                };
3816
3817                if point_axis_constraint_references_point(constraint, end_id) {
3818                    constraints_to_delete_set.insert(obj.id);
3819                }
3820            }
3821        }
3822
3823        // Find point-point constraints on end endpoint to migrate
3824        if let Some(end_id) = original_end_point_id {
3825            let end_point_point_constraint_ids = find_point_point_coincident_constraints(end_id);
3826            for constraint_id in end_point_point_constraint_ids {
3827                // Identify the other point in the coincident constraint
3828                let other_point_id_opt = objects.iter().find_map(|obj| {
3829                    if obj.id != constraint_id {
3830                        return None;
3831                    }
3832                    let ObjectKind::Constraint { constraint } = &obj.kind else {
3833                        return None;
3834                    };
3835                    let Constraint::Coincident(coincident) = constraint else {
3836                        return None;
3837                    };
3838                    coincident.segment_ids().find(|&seg_id| seg_id != end_id)
3839                });
3840
3841                if let Some(other_point_id) = other_point_id_opt {
3842                    constraints_to_delete_set.insert(constraint_id);
3843                    // Migrate as point-point constraint to the new end endpoint
3844                    constraints_to_migrate.push(ConstraintToMigrate {
3845                        constraint_id,
3846                        other_entity_id: other_point_id,
3847                        is_point_point: true,
3848                        attach_to_endpoint: AttachToEndpoint::End,
3849                    });
3850                }
3851            }
3852        }
3853
3854        // Find point-segment constraints on end endpoint to migrate
3855        if let Some(end_id) = original_end_point_id {
3856            let end_point_segment_constraints = find_point_segment_coincident_constraints(end_id);
3857            for constraint_json in end_point_segment_constraints {
3858                if let Some(constraint_id_usize) = constraint_json
3859                    .get("constraintId")
3860                    .and_then(|v| v.as_u64())
3861                    .map(|id| id as usize)
3862                {
3863                    let constraint_id = ObjectId(constraint_id_usize);
3864                    constraints_to_delete_set.insert(constraint_id);
3865                    // Add to migrate list (simplified)
3866                    if let Some(other_id_usize) = constraint_json
3867                        .get("segmentOrPointId")
3868                        .and_then(|v| v.as_u64())
3869                        .map(|id| id as usize)
3870                    {
3871                        constraints_to_migrate.push(ConstraintToMigrate {
3872                            constraint_id,
3873                            other_entity_id: ObjectId(other_id_usize),
3874                            is_point_point: false,
3875                            attach_to_endpoint: AttachToEndpoint::End,
3876                        });
3877                    }
3878                }
3879            }
3880        }
3881
3882        // Find point-segment constraints where the point is geometrically at the original end point
3883        // These should migrate to [newSegmentEndPointId, pointId] (point-point), not [pointId, newSegmentId] (point-segment)
3884        // We need to find these by checking all point-segment constraints involving the segment ID
3885        // and checking if the point is at the original end point
3886        if let Some(end_id) = original_end_point_id {
3887            for obj in objects {
3888                let ObjectKind::Constraint { constraint } = &obj.kind else {
3889                    continue;
3890                };
3891
3892                let Constraint::Coincident(coincident) = constraint else {
3893                    continue;
3894                };
3895
3896                // Only consider constraints that involve the segment ID but NOT the endpoint IDs directly
3897                // Note: We want to find constraints like [pointId, segmentId] where pointId is a point
3898                // that happens to be at the endpoint geometrically, but the constraint doesn't reference
3899                // the endpoint ID directly
3900                if !coincident.contains_segment(trim_spawn_id) {
3901                    continue;
3902                }
3903                // Skip constraints that involve endpoint IDs directly (those are handled by endpoint constraint migration)
3904                // But we still want to find constraints where a point (not an endpoint ID) is at the endpoint
3905                if let (Some(start_id), Some(end_id_val)) = (original_start_point_id, Some(end_id))
3906                    && coincident.segment_ids().any(|id| id == start_id || id == end_id_val)
3907                {
3908                    continue; // Skip constraints that involve endpoint IDs directly
3909                }
3910
3911                // Find the other entity (should be a point)
3912                let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
3913
3914                if let Some(other_id) = other_id {
3915                    // Check if the other entity is a point
3916                    if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
3917                        let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
3918                            continue;
3919                        };
3920
3921                        let Segment::Point(point) = other_segment else {
3922                            continue;
3923                        };
3924
3925                        // Get point coordinates in the trim internal unit
3926                        let point_coords = Coords2d {
3927                            x: number_to_unit(&point.position.x, default_unit),
3928                            y: number_to_unit(&point.position.y, default_unit),
3929                        };
3930
3931                        // Check if point is at original end point (geometrically)
3932                        // Use post-solve coordinates for original end point if available, otherwise use the coordinates we have
3933                        let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
3934                            if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
3935                                if let ObjectKind::Segment {
3936                                    segment: Segment::Point(end_point),
3937                                } = &end_point_obj.kind
3938                                {
3939                                    Some(Coords2d {
3940                                        x: number_to_unit(&end_point.position.x, default_unit),
3941                                        y: number_to_unit(&end_point.position.y, default_unit),
3942                                    })
3943                                } else {
3944                                    None
3945                                }
3946                            } else {
3947                                None
3948                            }
3949                        } else {
3950                            None
3951                        };
3952
3953                        let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
3954                        let dist_to_original_end = ((point_coords.x - reference_coords.x)
3955                            * (point_coords.x - reference_coords.x)
3956                            + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
3957                            .sqrt();
3958
3959                        if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
3960                            // Point is at the original end point - migrate as point-point constraint
3961                            // Check if there's already a point-point constraint between this point and the original end point
3962                            let has_point_point_constraint = find_point_point_coincident_constraints(end_id)
3963                                .iter()
3964                                .any(|&constraint_id| {
3965                                    if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
3966                                        if let ObjectKind::Constraint {
3967                                            constraint: Constraint::Coincident(coincident),
3968                                        } = &constraint_obj.kind
3969                                        {
3970                                            coincident.contains_segment(other_id)
3971                                        } else {
3972                                            false
3973                                        }
3974                                    } else {
3975                                        false
3976                                    }
3977                                });
3978
3979                            if !has_point_point_constraint {
3980                                // No existing point-point constraint - migrate as point-point constraint
3981                                constraints_to_migrate.push(ConstraintToMigrate {
3982                                    constraint_id: obj.id,
3983                                    other_entity_id: other_id,
3984                                    is_point_point: true, // Convert to point-point constraint
3985                                    attach_to_endpoint: AttachToEndpoint::End, // Attach to new segment's end
3986                                });
3987                            }
3988                            // Always delete the old point-segment constraint (whether we migrate or not)
3989                            constraints_to_delete_set.insert(obj.id);
3990                        }
3991                    }
3992                }
3993            }
3994        }
3995
3996        // Find point-segment constraints on the segment body (not at endpoints)
3997        // These are constraints [pointId, segmentId] where the point is on the segment body
3998        // They should be migrated to [pointId, newSegmentId] if the point is after the split point
3999        let split_point = right_trim_coords; // Use right trim coords as split point
4000        let segment_start_coords = match segment {
4001            Segment::Line(_) => {
4002                get_position_coords_for_line(trim_spawn_segment, LineEndpoint::Start, objects, default_unit)
4003            }
4004            Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Start, objects, default_unit),
4005            _ => None,
4006        };
4007        let segment_end_coords = match segment {
4008            Segment::Line(_) => {
4009                get_position_coords_for_line(trim_spawn_segment, LineEndpoint::End, objects, default_unit)
4010            }
4011            Segment::Arc(_) => get_position_coords_from_arc(trim_spawn_segment, ArcPoint::End, objects, default_unit),
4012            _ => None,
4013        };
4014        let segment_center_coords = match segment {
4015            Segment::Line(_) => None,
4016            Segment::Arc(_) => {
4017                get_position_coords_from_arc(trim_spawn_segment, ArcPoint::Center, objects, default_unit)
4018            }
4019            _ => None,
4020        };
4021
4022        if let (Some(start_coords), Some(end_coords)) = (segment_start_coords, segment_end_coords) {
4023            // Calculate split point parametric position
4024            let split_point_t_opt = match segment {
4025                Segment::Line(_) => Some(project_point_onto_segment(split_point, start_coords, end_coords)),
4026                Segment::Arc(_) => segment_center_coords
4027                    .map(|center| project_point_onto_arc(split_point, center, start_coords, end_coords)),
4028                _ => None,
4029            };
4030
4031            if let Some(split_point_t) = split_point_t_opt {
4032                // Find all coincident constraints involving the segment
4033                for obj in objects {
4034                    let ObjectKind::Constraint { constraint } = &obj.kind else {
4035                        continue;
4036                    };
4037
4038                    let Constraint::Coincident(coincident) = constraint else {
4039                        continue;
4040                    };
4041
4042                    // Check if constraint involves the segment being split
4043                    if !coincident.contains_segment(trim_spawn_id) {
4044                        continue;
4045                    }
4046
4047                    // Skip if constraint also involves endpoint IDs directly (those are handled separately)
4048                    if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id)
4049                        && coincident.segment_ids().any(|id| id == start_id || id == end_id)
4050                    {
4051                        continue;
4052                    }
4053
4054                    // Find the other entity in the constraint
4055                    let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
4056
4057                    if let Some(other_id) = other_id {
4058                        // Check if the other entity is a point
4059                        if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
4060                            let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
4061                                continue;
4062                            };
4063
4064                            let Segment::Point(point) = other_segment else {
4065                                continue;
4066                            };
4067
4068                            // Get point coordinates in the trim internal unit
4069                            let point_coords = Coords2d {
4070                                x: number_to_unit(&point.position.x, default_unit),
4071                                y: number_to_unit(&point.position.y, default_unit),
4072                            };
4073
4074                            // Project the point onto the segment to get its parametric position
4075                            let point_t = match segment {
4076                                Segment::Line(_) => project_point_onto_segment(point_coords, start_coords, end_coords),
4077                                Segment::Arc(_) => {
4078                                    if let Some(center) = segment_center_coords {
4079                                        project_point_onto_arc(point_coords, center, start_coords, end_coords)
4080                                    } else {
4081                                        continue; // Skip this constraint if no center
4082                                    }
4083                                }
4084                                _ => continue, // Skip non-line/arc segments
4085                            };
4086
4087                            // Check if point is at the original end point (skip if so - already handled above)
4088                            // Use post-solve coordinates for original end point if available
4089                            let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
4090                                if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
4091                                    if let ObjectKind::Segment {
4092                                        segment: Segment::Point(end_point),
4093                                    } = &end_point_obj.kind
4094                                    {
4095                                        Some(Coords2d {
4096                                            x: number_to_unit(&end_point.position.x, default_unit),
4097                                            y: number_to_unit(&end_point.position.y, default_unit),
4098                                        })
4099                                    } else {
4100                                        None
4101                                    }
4102                                } else {
4103                                    None
4104                                }
4105                            } else {
4106                                None
4107                            };
4108
4109                            let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
4110                            let dist_to_original_end = ((point_coords.x - reference_coords.x)
4111                                * (point_coords.x - reference_coords.x)
4112                                + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
4113                                .sqrt();
4114
4115                            if dist_to_original_end < EPSILON_POINT_ON_SEGMENT {
4116                                // This should have been handled in the first loop, but if we find it here,
4117                                // make sure it's deleted (it might have been missed due to filtering)
4118                                // Also check if we should migrate it as point-point constraint
4119                                let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
4120                                    find_point_point_coincident_constraints(end_id)
4121                                        .iter()
4122                                        .any(|&constraint_id| {
4123                                            if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
4124                                            {
4125                                                if let ObjectKind::Constraint {
4126                                                    constraint: Constraint::Coincident(coincident),
4127                                                } = &constraint_obj.kind
4128                                                {
4129                                                    coincident.contains_segment(other_id)
4130                                                } else {
4131                                                    false
4132                                                }
4133                                            } else {
4134                                                false
4135                                            }
4136                                        })
4137                                } else {
4138                                    false
4139                                };
4140
4141                                if !has_point_point_constraint {
4142                                    // No existing point-point constraint - migrate as point-point constraint
4143                                    constraints_to_migrate.push(ConstraintToMigrate {
4144                                        constraint_id: obj.id,
4145                                        other_entity_id: other_id,
4146                                        is_point_point: true, // Convert to point-point constraint
4147                                        attach_to_endpoint: AttachToEndpoint::End, // Attach to new segment's end
4148                                    });
4149                                }
4150                                // Always delete the old point-segment constraint
4151                                constraints_to_delete_set.insert(obj.id);
4152                                continue; // Already handled as point-point constraint migration above
4153                            }
4154
4155                            // Check if point is at the current start endpoint (skip if so - handled separately)
4156                            let dist_to_start = ((point_coords.x - start_coords.x) * (point_coords.x - start_coords.x)
4157                                + (point_coords.y - start_coords.y) * (point_coords.y - start_coords.y))
4158                                .sqrt();
4159                            let is_at_start = (point_t - 0.0).abs() < EPSILON_POINT_ON_SEGMENT
4160                                || dist_to_start < EPSILON_POINT_ON_SEGMENT;
4161
4162                            if is_at_start {
4163                                continue; // Handled by endpoint constraint migration
4164                            }
4165
4166                            // Check if point is at the split point (don't migrate - would pull halves together)
4167                            let dist_to_split = (point_t - split_point_t).abs();
4168                            if dist_to_split < EPSILON_POINT_ON_SEGMENT * 100.0 {
4169                                continue; // Too close to split point
4170                            }
4171
4172                            // If point is after split point (closer to end), migrate to new segment
4173                            if point_t > split_point_t {
4174                                constraints_to_migrate.push(ConstraintToMigrate {
4175                                    constraint_id: obj.id,
4176                                    other_entity_id: other_id,
4177                                    is_point_point: false, // Keep as point-segment, but replace the segment
4178                                    attach_to_endpoint: AttachToEndpoint::Segment, // Replace old segment with new segment
4179                                });
4180                                constraints_to_delete_set.insert(obj.id);
4181                            }
4182                        }
4183                    }
4184                }
4185            } // End of if let Some(split_point_t)
4186        } // End of if let (Some(start_coords), Some(end_coords))
4187
4188        // Find distance constraints that reference the segment being split
4189        // These need to be deleted and re-added with new endpoints after split
4190        // BUT: For arcs, we need to exclude distance constraints that reference the center point
4191        // (those will be migrated separately in the execution code)
4192        let distance_constraint_ids_for_split = find_distance_constraints_for_segment(trim_spawn_id);
4193
4194        // Get the center point ID if this is an arc, so we can exclude center point constraints
4195        let arc_center_point_id: Option<ObjectId> = match segment {
4196            Segment::Arc(arc) => Some(arc.center),
4197            _ => None,
4198        };
4199
4200        for constraint_id in distance_constraint_ids_for_split {
4201            // Skip if this is a center point constraint for an arc (will be migrated separately)
4202            if let Some(center_id) = arc_center_point_id {
4203                // Check if this constraint references the center point
4204                if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id)
4205                    && let ObjectKind::Constraint { constraint } = &constraint_obj.kind
4206                    && let Constraint::Distance(distance) = constraint
4207                    && distance.contains_point(center_id)
4208                {
4209                    // This is a center point constraint - skip deletion, it will be migrated
4210                    continue;
4211                }
4212            }
4213
4214            constraints_to_delete_set.insert(constraint_id);
4215        }
4216
4217        // Midpoint constraints become stale after trim changes the owning
4218        // segment's extent, so delete them instead of migrating them.
4219        for obj in objects {
4220            let ObjectKind::Constraint { constraint } = &obj.kind else {
4221                continue;
4222            };
4223
4224            let Constraint::Midpoint(midpoint) = constraint else {
4225                continue;
4226            };
4227
4228            let references_trimmed_segment = midpoint.segment == trim_spawn_id;
4229            let references_trimmed_endpoint = original_start_point_id.is_some_and(|id| midpoint.point == id)
4230                || original_end_point_id.is_some_and(|id| midpoint.point == id);
4231
4232            if references_trimmed_segment || references_trimmed_endpoint {
4233                constraints_to_delete_set.insert(obj.id);
4234            }
4235        }
4236
4237        // Find angle constraints (Parallel, Perpendicular, Horizontal, Vertical) that reference the segment being split
4238        // Note: We don't delete these - they still apply to the original (trimmed) segment
4239        // We'll add new constraints for the new segment in the execution code
4240
4241        // Catch-all: Find any remaining point-segment constraints involving the segment
4242        // that we might have missed (e.g., due to coordinate precision issues)
4243        // This ensures we don't leave orphaned constraints
4244        for obj in objects {
4245            let ObjectKind::Constraint { constraint } = &obj.kind else {
4246                continue;
4247            };
4248
4249            let Constraint::Coincident(coincident) = constraint else {
4250                continue;
4251            };
4252
4253            // Only consider constraints that involve the segment ID
4254            if !coincident.contains_segment(trim_spawn_id) {
4255                continue;
4256            }
4257
4258            // Skip if already marked for deletion
4259            if constraints_to_delete_set.contains(&obj.id) {
4260                continue;
4261            }
4262
4263            // Skip if this constraint involves an endpoint directly (handled separately)
4264            // BUT: if the other entity is a point that's at the original end point geometrically,
4265            // we still want to handle it here even if it's not the same point object
4266            // So we'll check this after we verify the other entity is a point and check its coordinates
4267
4268            // Find the other entity (should be a point)
4269            let other_id = coincident.segment_ids().find(|&seg_id| seg_id != trim_spawn_id);
4270
4271            if let Some(other_id) = other_id {
4272                // Check if the other entity is a point
4273                if let Some(other_obj) = objects.iter().find(|o| o.id == other_id) {
4274                    let ObjectKind::Segment { segment: other_segment } = &other_obj.kind else {
4275                        continue;
4276                    };
4277
4278                    let Segment::Point(point) = other_segment else {
4279                        continue;
4280                    };
4281
4282                    // Skip if this constraint involves an endpoint directly (handled separately)
4283                    // BUT: if the point is at the original end point geometrically, we still want to handle it
4284                    let _is_endpoint_constraint =
4285                        if let (Some(start_id), Some(end_id)) = (original_start_point_id, original_end_point_id) {
4286                            coincident.segment_ids().any(|id| id == start_id || id == end_id)
4287                        } else {
4288                            false
4289                        };
4290
4291                    // Get point coordinates in the trim internal unit
4292                    let point_coords = Coords2d {
4293                        x: number_to_unit(&point.position.x, default_unit),
4294                        y: number_to_unit(&point.position.y, default_unit),
4295                    };
4296
4297                    // Check if point is at original end point (with relaxed tolerance for catch-all)
4298                    let original_end_point_post_solve_coords = if let Some(end_id) = original_end_point_id {
4299                        if let Some(end_point_obj) = objects.iter().find(|o| o.id == end_id) {
4300                            if let ObjectKind::Segment {
4301                                segment: Segment::Point(end_point),
4302                            } = &end_point_obj.kind
4303                            {
4304                                Some(Coords2d {
4305                                    x: number_to_unit(&end_point.position.x, default_unit),
4306                                    y: number_to_unit(&end_point.position.y, default_unit),
4307                                })
4308                            } else {
4309                                None
4310                            }
4311                        } else {
4312                            None
4313                        }
4314                    } else {
4315                        None
4316                    };
4317
4318                    let reference_coords = original_end_point_post_solve_coords.unwrap_or(original_end_coords);
4319                    let dist_to_original_end = ((point_coords.x - reference_coords.x)
4320                        * (point_coords.x - reference_coords.x)
4321                        + (point_coords.y - reference_coords.y) * (point_coords.y - reference_coords.y))
4322                        .sqrt();
4323
4324                    // Use a slightly more relaxed tolerance for catch-all to catch edge cases
4325                    // Also handle endpoint constraints that might have been missed
4326                    let is_at_original_end = dist_to_original_end < EPSILON_POINT_ON_SEGMENT * 2.0;
4327
4328                    if is_at_original_end {
4329                        // Point is at or very close to original end point - delete the constraint
4330                        // Check if we should migrate it as point-point constraint
4331                        let has_point_point_constraint = if let Some(end_id) = original_end_point_id {
4332                            find_point_point_coincident_constraints(end_id)
4333                                .iter()
4334                                .any(|&constraint_id| {
4335                                    if let Some(constraint_obj) = objects.iter().find(|o| o.id == constraint_id) {
4336                                        if let ObjectKind::Constraint {
4337                                            constraint: Constraint::Coincident(coincident),
4338                                        } = &constraint_obj.kind
4339                                        {
4340                                            coincident.contains_segment(other_id)
4341                                        } else {
4342                                            false
4343                                        }
4344                                    } else {
4345                                        false
4346                                    }
4347                                })
4348                        } else {
4349                            false
4350                        };
4351
4352                        if !has_point_point_constraint {
4353                            // No existing point-point constraint - migrate as point-point constraint
4354                            constraints_to_migrate.push(ConstraintToMigrate {
4355                                constraint_id: obj.id,
4356                                other_entity_id: other_id,
4357                                is_point_point: true, // Convert to point-point constraint
4358                                attach_to_endpoint: AttachToEndpoint::End, // Attach to new segment's end
4359                            });
4360                        }
4361                        // Always delete the old point-segment constraint
4362                        constraints_to_delete_set.insert(obj.id);
4363                    }
4364                }
4365            }
4366        }
4367
4368        // Create split segment operation
4369        let constraints_to_delete: Vec<ObjectId> = constraints_to_delete_set.iter().copied().collect();
4370        let plan = TrimPlan::SplitSegment {
4371            segment_id: trim_spawn_id,
4372            left_trim_coords,
4373            right_trim_coords,
4374            original_end_coords,
4375            left_side: Box::new(left_side.clone()),
4376            right_side: Box::new(right_side.clone()),
4377            left_side_coincident_data: CoincidentData {
4378                intersecting_seg_id: left_intersecting_seg_id,
4379                intersecting_endpoint_point_id: left_coincident_data.intersecting_endpoint_point_id,
4380                existing_point_segment_constraint_id: left_coincident_data.existing_point_segment_constraint_id,
4381            },
4382            right_side_coincident_data: CoincidentData {
4383                intersecting_seg_id: right_intersecting_seg_id,
4384                intersecting_endpoint_point_id: right_coincident_data.intersecting_endpoint_point_id,
4385                existing_point_segment_constraint_id: right_coincident_data.existing_point_segment_constraint_id,
4386            },
4387            constraints_to_migrate,
4388            constraints_to_delete,
4389        };
4390
4391        return Ok(plan);
4392    }
4393
4394    // Only three strategy cases should exist: simple trim (endpoint/endpoint),
4395    // tail cut (intersection+endpoint), or split (intersection+intersection).
4396    // If we get here, trim termination pairing was unexpected or a new variant
4397    // was added without updating the strategy mapping.
4398    Err(format!(
4399        "Unsupported trim termination combination: left={:?} right={:?}",
4400        left_side, right_side
4401    ))
4402}
4403
4404/// Execute the trim operations determined by the trim strategy
4405///
4406/// Once we have a trim strategy, it then needs to be executed. This function is separate just to keep
4407/// one phase just collecting info (`build_trim_plan` + `lower_trim_plan`), and the other actually mutating things.
4408///
4409/// This function takes the list of trim operations from `lower_trim_plan` and executes them, which may include:
4410/// - Deleting segments (SimpleTrim)
4411/// - Editing segment endpoints (EditSegment)
4412/// - Adding coincident constraints (AddCoincidentConstraint)
4413/// - Splitting segments (SplitSegment)
4414/// - Migrating constraints (MigrateConstraint)
4415pub(crate) async fn execute_trim_operations_simple(
4416    strategy: Vec<TrimOperation>,
4417    current_scene_graph_delta: &crate::frontend::api::SceneGraphDelta,
4418    frontend: &mut crate::frontend::FrontendState,
4419    ctx: &crate::ExecutorContext,
4420    version: crate::frontend::api::Version,
4421    sketch_id: ObjectId,
4422) -> Result<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta), String> {
4423    use crate::frontend::SketchApi;
4424    use crate::frontend::sketch::Constraint;
4425    use crate::frontend::sketch::ExistingSegmentCtor;
4426    use crate::frontend::sketch::SegmentCtor;
4427
4428    let default_unit = frontend.default_length_unit();
4429
4430    let mut op_index = 0;
4431    let mut last_result: Option<(crate::frontend::api::SourceDelta, crate::frontend::api::SceneGraphDelta)> = None;
4432    let mut invalidates_ids = false;
4433
4434    while op_index < strategy.len() {
4435        let mut consumed_ops = 1;
4436        let operation_result = match &strategy[op_index] {
4437            TrimOperation::SimpleTrim { segment_to_trim_id } => {
4438                // Delete the segment
4439                frontend
4440                    .delete_objects(
4441                        ctx,
4442                        version,
4443                        sketch_id,
4444                        Vec::new(),                // constraint_ids
4445                        vec![*segment_to_trim_id], // segment_ids
4446                    )
4447                    .await
4448                    .map_err(|e| format!("Failed to delete segment: {}", e.error.message()))
4449            }
4450            TrimOperation::EditSegment {
4451                segment_id,
4452                ctor,
4453                endpoint_changed,
4454                additional_edited_segment_ids,
4455            } => {
4456                // Try to batch tail-cut sequence: EditSegment + AddCoincidentConstraint (+ DeleteConstraints)
4457                // This matches the batching logic in kcl-wasm-lib/src/api.rs
4458                if op_index + 1 < strategy.len() {
4459                    if let TrimOperation::AddCoincidentConstraint {
4460                        segment_id: coincident_seg_id,
4461                        endpoint_changed: coincident_endpoint_changed,
4462                        segment_or_point_to_make_coincident_to,
4463                        intersecting_endpoint_point_id,
4464                    } = &strategy[op_index + 1]
4465                    {
4466                        if segment_id == coincident_seg_id && endpoint_changed == coincident_endpoint_changed {
4467                            // This is a tail-cut sequence - batch it!
4468                            let mut delete_constraint_ids: Vec<ObjectId> = Vec::new();
4469                            consumed_ops = 2;
4470
4471                            if op_index + 2 < strategy.len()
4472                                && let TrimOperation::DeleteConstraints { constraint_ids } = &strategy[op_index + 2]
4473                            {
4474                                delete_constraint_ids = constraint_ids.to_vec();
4475                                consumed_ops = 3;
4476                            }
4477
4478                            // Use ctor directly
4479                            let segment_ctor = ctor.clone();
4480
4481                            // Get endpoint point id from current scene graph (IDs stay the same after edit)
4482                            let edited_segment = current_scene_graph_delta
4483                                .new_graph
4484                                .objects
4485                                .iter()
4486                                .find(|obj| obj.id == *segment_id)
4487                                .ok_or_else(|| format!("Failed to find segment {} for tail-cut batch", segment_id.0))?;
4488
4489                            let endpoint_point_id = match &edited_segment.kind {
4490                                crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4491                                    crate::frontend::sketch::Segment::Line(line) => {
4492                                        if *endpoint_changed == EndpointChanged::Start {
4493                                            line.start
4494                                        } else {
4495                                            line.end
4496                                        }
4497                                    }
4498                                    crate::frontend::sketch::Segment::Arc(arc) => {
4499                                        if *endpoint_changed == EndpointChanged::Start {
4500                                            arc.start
4501                                        } else {
4502                                            arc.end
4503                                        }
4504                                    }
4505                                    _ => {
4506                                        return Err("Unsupported segment type for tail-cut batch".to_string());
4507                                    }
4508                                },
4509                                _ => {
4510                                    return Err("Edited object is not a segment (tail-cut batch)".to_string());
4511                                }
4512                            };
4513
4514                            let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
4515                                vec![endpoint_point_id.into(), (*point_id).into()]
4516                            } else {
4517                                vec![
4518                                    endpoint_point_id.into(),
4519                                    (*segment_or_point_to_make_coincident_to).into(),
4520                                ]
4521                            };
4522
4523                            let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4524                                segments: coincident_segments,
4525                            });
4526
4527                            let segment_to_edit = ExistingSegmentCtor {
4528                                id: *segment_id,
4529                                ctor: segment_ctor,
4530                            };
4531
4532                            // Batch the operations - this is the key optimization!
4533                            // Note: consumed_ops is set above (2 or 3), and we'll use it after the match
4534                            frontend
4535                                .batch_tail_cut_operations(
4536                                    ctx,
4537                                    version,
4538                                    sketch_id,
4539                                    vec![segment_to_edit],
4540                                    vec![constraint],
4541                                    delete_constraint_ids,
4542                                    additional_edited_segment_ids.clone(),
4543                                )
4544                                .await
4545                                .map_err(|e| format!("Failed to batch tail-cut operations: {}", e.error.message()))
4546                        } else {
4547                            // Not same segment/endpoint - execute EditSegment normally
4548                            let segment_to_edit = ExistingSegmentCtor {
4549                                id: *segment_id,
4550                                ctor: ctor.clone(),
4551                            };
4552
4553                            frontend
4554                                .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4555                                .await
4556                                .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4557                        }
4558                    } else {
4559                        // Not followed by AddCoincidentConstraint - execute EditSegment normally
4560                        let segment_to_edit = ExistingSegmentCtor {
4561                            id: *segment_id,
4562                            ctor: ctor.clone(),
4563                        };
4564
4565                        frontend
4566                            .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4567                            .await
4568                            .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4569                    }
4570                } else {
4571                    // No following op to batch with - execute EditSegment normally
4572                    let segment_to_edit = ExistingSegmentCtor {
4573                        id: *segment_id,
4574                        ctor: ctor.clone(),
4575                    };
4576
4577                    frontend
4578                        .edit_segments(ctx, version, sketch_id, vec![segment_to_edit])
4579                        .await
4580                        .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))
4581                }
4582            }
4583            TrimOperation::AddCoincidentConstraint {
4584                segment_id,
4585                endpoint_changed,
4586                segment_or_point_to_make_coincident_to,
4587                intersecting_endpoint_point_id,
4588            } => {
4589                // Find the edited segment to get the endpoint point ID
4590                let edited_segment = current_scene_graph_delta
4591                    .new_graph
4592                    .objects
4593                    .iter()
4594                    .find(|obj| obj.id == *segment_id)
4595                    .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
4596
4597                // Get the endpoint ID after editing
4598                let new_segment_endpoint_point_id = match &edited_segment.kind {
4599                    crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4600                        crate::frontend::sketch::Segment::Line(line) => {
4601                            if *endpoint_changed == EndpointChanged::Start {
4602                                line.start
4603                            } else {
4604                                line.end
4605                            }
4606                        }
4607                        crate::frontend::sketch::Segment::Arc(arc) => {
4608                            if *endpoint_changed == EndpointChanged::Start {
4609                                arc.start
4610                            } else {
4611                                arc.end
4612                            }
4613                        }
4614                        _ => {
4615                            return Err("Unsupported segment type for addCoincidentConstraint".to_string());
4616                        }
4617                    },
4618                    _ => {
4619                        return Err("Edited object is not a segment".to_string());
4620                    }
4621                };
4622
4623                // Determine coincident segments
4624                let coincident_segments = if let Some(point_id) = intersecting_endpoint_point_id {
4625                    vec![new_segment_endpoint_point_id.into(), (*point_id).into()]
4626                } else {
4627                    vec![
4628                        new_segment_endpoint_point_id.into(),
4629                        (*segment_or_point_to_make_coincident_to).into(),
4630                    ]
4631                };
4632
4633                let constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4634                    segments: coincident_segments,
4635                });
4636
4637                frontend
4638                    .add_constraint(ctx, version, sketch_id, constraint)
4639                    .await
4640                    .map_err(|e| format!("Failed to add constraint: {}", e.error.message()))
4641            }
4642            TrimOperation::DeleteConstraints { constraint_ids } => {
4643                // Delete constraints
4644                let constraint_object_ids: Vec<ObjectId> = constraint_ids.to_vec();
4645
4646                frontend
4647                    .delete_objects(
4648                        ctx,
4649                        version,
4650                        sketch_id,
4651                        constraint_object_ids,
4652                        Vec::new(), // segment_ids
4653                    )
4654                    .await
4655                    .map_err(|e| format!("Failed to delete constraints: {}", e.error.message()))
4656            }
4657            TrimOperation::ReplaceCircleWithArc {
4658                circle_id,
4659                arc_start_coords,
4660                arc_end_coords,
4661                arc_start_termination,
4662                arc_end_termination,
4663            } => {
4664                // Replace a circle with a single arc and re-attach coincident constraints.
4665                let original_circle = current_scene_graph_delta
4666                    .new_graph
4667                    .objects
4668                    .iter()
4669                    .find(|obj| obj.id == *circle_id)
4670                    .ok_or_else(|| format!("Failed to find original circle {}", circle_id.0))?;
4671
4672                let (original_circle_start_id, original_circle_center_id, circle_ctor) = match &original_circle.kind {
4673                    crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4674                        crate::frontend::sketch::Segment::Circle(circle) => match &circle.ctor {
4675                            SegmentCtor::Circle(circle_ctor) => (circle.start, circle.center, circle_ctor.clone()),
4676                            _ => return Err("Circle does not have a Circle ctor".to_string()),
4677                        },
4678                        _ => return Err("Original segment is not a circle".to_string()),
4679                    },
4680                    _ => return Err("Original object is not a segment".to_string()),
4681                };
4682
4683                let units = match &circle_ctor.start.x {
4684                    crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
4685                    _ => crate::pretty::NumericSuffix::Mm,
4686                };
4687
4688                let coords_to_point_expr = |coords: Coords2d| crate::frontend::sketch::Point2d {
4689                    x: crate::frontend::api::Expr::Var(unit_to_number(coords.x, default_unit, units)),
4690                    y: crate::frontend::api::Expr::Var(unit_to_number(coords.y, default_unit, units)),
4691                };
4692
4693                let arc_ctor = SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
4694                    start: coords_to_point_expr(*arc_start_coords),
4695                    end: coords_to_point_expr(*arc_end_coords),
4696                    center: circle_ctor.center.clone(),
4697                    construction: circle_ctor.construction,
4698                });
4699
4700                let (_add_source_delta, add_scene_graph_delta) = frontend
4701                    .add_segment(ctx, version, sketch_id, arc_ctor, None)
4702                    .await
4703                    .map_err(|e| format!("Failed to add arc while replacing circle: {}", e.error.message()))?;
4704                invalidates_ids = invalidates_ids || add_scene_graph_delta.invalidates_ids;
4705
4706                let new_arc_id = *add_scene_graph_delta
4707                    .new_objects
4708                    .iter()
4709                    .find(|&id| {
4710                        add_scene_graph_delta
4711                            .new_graph
4712                            .objects
4713                            .iter()
4714                            .find(|o| o.id == *id)
4715                            .is_some_and(|obj| {
4716                                matches!(
4717                                    &obj.kind,
4718                                    crate::frontend::api::ObjectKind::Segment { segment }
4719                                        if matches!(segment, crate::frontend::sketch::Segment::Arc(_))
4720                                )
4721                            })
4722                    })
4723                    .ok_or_else(|| "Failed to find newly created arc segment".to_string())?;
4724
4725                let new_arc_obj = add_scene_graph_delta
4726                    .new_graph
4727                    .objects
4728                    .iter()
4729                    .find(|obj| obj.id == new_arc_id)
4730                    .ok_or_else(|| format!("New arc segment not found {}", new_arc_id.0))?;
4731                let (new_arc_start_id, new_arc_end_id, new_arc_center_id) = match &new_arc_obj.kind {
4732                    crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4733                        crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, arc.center),
4734                        _ => return Err("New segment is not an arc".to_string()),
4735                    },
4736                    _ => return Err("New arc object is not a segment".to_string()),
4737                };
4738
4739                let constraint_segments_for =
4740                    |arc_endpoint_id: ObjectId,
4741                     term: &TrimTermination|
4742                     -> Result<Vec<crate::frontend::sketch::ConstraintSegment>, String> {
4743                        match term {
4744                            TrimTermination::Intersection {
4745                                intersecting_seg_id, ..
4746                            } => Ok(vec![arc_endpoint_id.into(), (*intersecting_seg_id).into()]),
4747                            TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4748                                other_segment_point_id,
4749                                ..
4750                            } => Ok(vec![arc_endpoint_id.into(), (*other_segment_point_id).into()]),
4751                            TrimTermination::SegEndPoint { .. } => {
4752                                Err("Circle replacement endpoint cannot terminate at seg endpoint".to_string())
4753                            }
4754                        }
4755                    };
4756
4757                let start_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4758                    segments: constraint_segments_for(new_arc_start_id, arc_start_termination)?,
4759                });
4760                let (_c1_source_delta, c1_scene_graph_delta) = frontend
4761                    .add_constraint(ctx, version, sketch_id, start_constraint)
4762                    .await
4763                    .map_err(|e| format!("Failed to add start coincident on replaced arc: {}", e.error.message()))?;
4764                invalidates_ids = invalidates_ids || c1_scene_graph_delta.invalidates_ids;
4765
4766                let end_constraint = Constraint::Coincident(crate::frontend::sketch::Coincident {
4767                    segments: constraint_segments_for(new_arc_end_id, arc_end_termination)?,
4768                });
4769                let (_c2_source_delta, c2_scene_graph_delta) = frontend
4770                    .add_constraint(ctx, version, sketch_id, end_constraint)
4771                    .await
4772                    .map_err(|e| format!("Failed to add end coincident on replaced arc: {}", e.error.message()))?;
4773                invalidates_ids = invalidates_ids || c2_scene_graph_delta.invalidates_ids;
4774
4775                let mut termination_point_ids: Vec<ObjectId> = Vec::new();
4776                for term in [arc_start_termination, arc_end_termination] {
4777                    if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
4778                        other_segment_point_id,
4779                        ..
4780                    } = term.as_ref()
4781                    {
4782                        termination_point_ids.push(*other_segment_point_id);
4783                    }
4784                }
4785
4786                // Migrate constraints that reference the original circle segment or points.
4787                // This preserves authored constraints (e.g. radius/tangent/coincident) when
4788                // a trim converts a circle into an arc.
4789                let rewrite_map = std::collections::HashMap::from([
4790                    (*circle_id, new_arc_id),
4791                    (original_circle_center_id, new_arc_center_id),
4792                    (original_circle_start_id, new_arc_start_id),
4793                ]);
4794                let rewrite_ids: std::collections::HashSet<ObjectId> = rewrite_map.keys().copied().collect();
4795
4796                let mut migrated_constraints: Vec<Constraint> = Vec::new();
4797                for obj in &current_scene_graph_delta.new_graph.objects {
4798                    let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4799                        continue;
4800                    };
4801
4802                    // Keep this exhaustive so new constraints must declare how
4803                    // circle-to-arc trim should migrate or ignore them.
4804                    match constraint {
4805                        Constraint::Coincident(coincident) => {
4806                            if !constraint_segments_reference_any(&coincident.segments, &rewrite_ids) {
4807                                continue;
4808                            }
4809
4810                            // If the original coincident is circle<->point for a point that is
4811                            // already used as a trim termination, endpoint coincident constraints
4812                            // already preserve that relationship.
4813                            if coincident.contains_segment(*circle_id)
4814                                && coincident
4815                                    .segment_ids()
4816                                    .filter(|id| *id != *circle_id)
4817                                    .any(|id| termination_point_ids.contains(&id))
4818                            {
4819                                continue;
4820                            }
4821
4822                            let Some(Constraint::Coincident(migrated_coincident)) =
4823                                rewrite_constraint_with_map(constraint, &rewrite_map)
4824                            else {
4825                                continue;
4826                            };
4827
4828                            // Skip redundant migration when a previous point-segment circle
4829                            // coincident would become point-segment arc coincident at an arc
4830                            // endpoint that is already handled by explicit endpoint constraints.
4831                            let migrated_ids: Vec<ObjectId> = migrated_coincident
4832                                .segments
4833                                .iter()
4834                                .filter_map(|segment| match segment {
4835                                    crate::frontend::sketch::ConstraintSegment::Segment(id) => Some(*id),
4836                                    crate::frontend::sketch::ConstraintSegment::Origin(_) => None,
4837                                })
4838                                .collect();
4839                            if migrated_ids.contains(&new_arc_id)
4840                                && (migrated_ids.contains(&new_arc_start_id) || migrated_ids.contains(&new_arc_end_id))
4841                            {
4842                                continue;
4843                            }
4844
4845                            migrated_constraints.push(Constraint::Coincident(migrated_coincident));
4846                        }
4847                        Constraint::Distance(distance) => {
4848                            if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4849                                continue;
4850                            }
4851                            if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4852                                migrated_constraints.push(migrated);
4853                            }
4854                        }
4855                        Constraint::HorizontalDistance(distance) => {
4856                            if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4857                                continue;
4858                            }
4859                            if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4860                                migrated_constraints.push(migrated);
4861                            }
4862                        }
4863                        Constraint::VerticalDistance(distance) => {
4864                            if !constraint_segments_reference_any(&distance.points, &rewrite_ids) {
4865                                continue;
4866                            }
4867                            if let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map) {
4868                                migrated_constraints.push(migrated);
4869                            }
4870                        }
4871                        Constraint::Radius(radius) => {
4872                            if radius.arc == *circle_id
4873                                && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4874                            {
4875                                migrated_constraints.push(migrated);
4876                            }
4877                        }
4878                        Constraint::Diameter(diameter) => {
4879                            if diameter.arc == *circle_id
4880                                && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4881                            {
4882                                migrated_constraints.push(migrated);
4883                            }
4884                        }
4885                        Constraint::EqualRadius(equal_radius) => {
4886                            if equal_radius.input.contains(circle_id)
4887                                && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4888                            {
4889                                migrated_constraints.push(migrated);
4890                            }
4891                        }
4892                        Constraint::Tangent(tangent) => {
4893                            if tangent.input.contains(circle_id)
4894                                && let Some(migrated) = rewrite_constraint_with_map(constraint, &rewrite_map)
4895                            {
4896                                migrated_constraints.push(migrated);
4897                            }
4898                        }
4899                        Constraint::Angle(_)
4900                        | Constraint::Fixed(_)
4901                        | Constraint::Horizontal(_)
4902                        | Constraint::LinesEqualLength(_)
4903                        | Constraint::Midpoint(_)
4904                        | Constraint::Parallel(_)
4905                        | Constraint::Perpendicular(_)
4906                        | Constraint::Symmetric(_)
4907                        | Constraint::Vertical(_) => {}
4908                    }
4909                }
4910
4911                for constraint in migrated_constraints {
4912                    let (_source_delta, migrated_scene_graph_delta) = frontend
4913                        .add_constraint(ctx, version, sketch_id, constraint)
4914                        .await
4915                        .map_err(|e| format!("Failed to migrate circle constraint to arc: {}", e.error.message()))?;
4916                    invalidates_ids = invalidates_ids || migrated_scene_graph_delta.invalidates_ids;
4917                }
4918
4919                frontend
4920                    .delete_objects(ctx, version, sketch_id, Vec::new(), vec![*circle_id])
4921                    .await
4922                    .map_err(|e| format!("Failed to delete circle after arc replacement: {}", e.error.message()))
4923            }
4924            TrimOperation::SplitSegment {
4925                segment_id,
4926                left_trim_coords,
4927                right_trim_coords,
4928                original_end_coords,
4929                left_side,
4930                right_side,
4931                constraints_to_migrate,
4932                constraints_to_delete,
4933                ..
4934            } => {
4935                // SplitSegment is a complex multi-step operation
4936                // Ported from kcl-wasm-lib/src/api.rs execute_trim function
4937
4938                // Step 1: Find and validate original segment
4939                let original_segment = current_scene_graph_delta
4940                    .new_graph
4941                    .objects
4942                    .iter()
4943                    .find(|obj| obj.id == *segment_id)
4944                    .ok_or_else(|| format!("Failed to find original segment {}", segment_id.0))?;
4945
4946                // Extract point IDs from original segment
4947                let (original_segment_start_point_id, original_segment_end_point_id, original_segment_center_point_id) =
4948                    match &original_segment.kind {
4949                        crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4950                            crate::frontend::sketch::Segment::Line(line) => (Some(line.start), Some(line.end), None),
4951                            crate::frontend::sketch::Segment::Arc(arc) => {
4952                                (Some(arc.start), Some(arc.end), Some(arc.center))
4953                            }
4954                            _ => (None, None, None),
4955                        },
4956                        _ => (None, None, None),
4957                    };
4958
4959                // Store center point constraints to migrate BEFORE edit_segments modifies the scene graph
4960                let mut center_point_constraints_to_migrate: Vec<(Constraint, ObjectId)> = Vec::new();
4961                if let Some(original_center_id) = original_segment_center_point_id {
4962                    for obj in &current_scene_graph_delta.new_graph.objects {
4963                        let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
4964                            continue;
4965                        };
4966
4967                        // Find coincident constraints that reference the original center point
4968                        if let Constraint::Coincident(coincident) = constraint
4969                            && coincident.contains_segment(original_center_id)
4970                        {
4971                            center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
4972                        }
4973
4974                        // Find distance constraints that reference the original center point
4975                        if let Constraint::Distance(distance) = constraint
4976                            && distance.contains_point(original_center_id)
4977                        {
4978                            center_point_constraints_to_migrate.push((constraint.clone(), original_center_id));
4979                        }
4980                    }
4981                }
4982
4983                // Extract segment and ctor
4984                let (_segment_type, original_ctor) = match &original_segment.kind {
4985                    crate::frontend::api::ObjectKind::Segment { segment } => match segment {
4986                        crate::frontend::sketch::Segment::Line(line) => ("Line", line.ctor.clone()),
4987                        crate::frontend::sketch::Segment::Arc(arc) => ("Arc", arc.ctor.clone()),
4988                        _ => {
4989                            return Err("Original segment is not a Line or Arc".to_string());
4990                        }
4991                    },
4992                    _ => {
4993                        return Err("Original object is not a segment".to_string());
4994                    }
4995                };
4996
4997                // Extract units from the existing ctor
4998                let units = match &original_ctor {
4999                    SegmentCtor::Line(line_ctor) => match &line_ctor.start.x {
5000                        crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
5001                        _ => crate::pretty::NumericSuffix::Mm,
5002                    },
5003                    SegmentCtor::Arc(arc_ctor) => match &arc_ctor.start.x {
5004                        crate::frontend::api::Expr::Var(v) | crate::frontend::api::Expr::Number(v) => v.units,
5005                        _ => crate::pretty::NumericSuffix::Mm,
5006                    },
5007                    _ => crate::pretty::NumericSuffix::Mm,
5008                };
5009
5010                // Helper to convert Coords2d (current trim unit) to Point2d in segment units.
5011                // No rounding here; rounding happens at final conversion to output if needed.
5012                let coords_to_point =
5013                    |coords: Coords2d| -> crate::frontend::sketch::Point2d<crate::frontend::api::Number> {
5014                        crate::frontend::sketch::Point2d {
5015                            x: unit_to_number(coords.x, default_unit, units),
5016                            y: unit_to_number(coords.y, default_unit, units),
5017                        }
5018                    };
5019
5020                // Convert Point2d<Number> to Point2d<Expr> for SegmentCtor
5021                let point_to_expr = |point: crate::frontend::sketch::Point2d<crate::frontend::api::Number>| -> crate::frontend::sketch::Point2d<crate::frontend::api::Expr> {
5022                    crate::frontend::sketch::Point2d {
5023                        x: crate::frontend::api::Expr::Var(point.x),
5024                        y: crate::frontend::api::Expr::Var(point.y),
5025                    }
5026                };
5027
5028                // Step 2: Create new segment (right side) first to get its IDs
5029                let new_segment_ctor = match &original_ctor {
5030                    SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
5031                        start: point_to_expr(coords_to_point(*right_trim_coords)),
5032                        end: point_to_expr(coords_to_point(*original_end_coords)),
5033                        construction: line_ctor.construction,
5034                    }),
5035                    SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
5036                        start: point_to_expr(coords_to_point(*right_trim_coords)),
5037                        end: point_to_expr(coords_to_point(*original_end_coords)),
5038                        center: arc_ctor.center.clone(),
5039                        construction: arc_ctor.construction,
5040                    }),
5041                    _ => {
5042                        return Err("Unsupported segment type for new segment".to_string());
5043                    }
5044                };
5045
5046                let (_add_source_delta, add_scene_graph_delta) = frontend
5047                    .add_segment(ctx, version, sketch_id, new_segment_ctor, None)
5048                    .await
5049                    .map_err(|e| format!("Failed to add new segment: {}", e.error.message()))?;
5050
5051                // Step 3: Find the newly created segment
5052                let new_segment_id = *add_scene_graph_delta
5053                    .new_objects
5054                    .iter()
5055                    .find(|&id| {
5056                        if let Some(obj) = add_scene_graph_delta.new_graph.objects.iter().find(|o| o.id == *id) {
5057                            matches!(
5058                                &obj.kind,
5059                                crate::frontend::api::ObjectKind::Segment { segment }
5060                                    if matches!(segment, crate::frontend::sketch::Segment::Line(_) | crate::frontend::sketch::Segment::Arc(_))
5061                            )
5062                        } else {
5063                            false
5064                        }
5065                    })
5066                    .ok_or_else(|| "Failed to find newly created segment".to_string())?;
5067
5068                let new_segment = add_scene_graph_delta
5069                    .new_graph
5070                    .objects
5071                    .iter()
5072                    .find(|o| o.id == new_segment_id)
5073                    .ok_or_else(|| format!("New segment not found with id {}", new_segment_id.0))?;
5074
5075                // Extract endpoint IDs
5076                let (new_segment_start_point_id, new_segment_end_point_id, new_segment_center_point_id) =
5077                    match &new_segment.kind {
5078                        crate::frontend::api::ObjectKind::Segment { segment } => match segment {
5079                            crate::frontend::sketch::Segment::Line(line) => (line.start, line.end, None),
5080                            crate::frontend::sketch::Segment::Arc(arc) => (arc.start, arc.end, Some(arc.center)),
5081                            _ => {
5082                                return Err("New segment is not a Line or Arc".to_string());
5083                            }
5084                        },
5085                        _ => {
5086                            return Err("New segment is not a segment".to_string());
5087                        }
5088                    };
5089
5090                // Step 4: Edit the original segment (trim left side)
5091                let edited_ctor = match &original_ctor {
5092                    SegmentCtor::Line(line_ctor) => SegmentCtor::Line(crate::frontend::sketch::LineCtor {
5093                        start: line_ctor.start.clone(),
5094                        end: point_to_expr(coords_to_point(*left_trim_coords)),
5095                        construction: line_ctor.construction,
5096                    }),
5097                    SegmentCtor::Arc(arc_ctor) => SegmentCtor::Arc(crate::frontend::sketch::ArcCtor {
5098                        start: arc_ctor.start.clone(),
5099                        end: point_to_expr(coords_to_point(*left_trim_coords)),
5100                        center: arc_ctor.center.clone(),
5101                        construction: arc_ctor.construction,
5102                    }),
5103                    _ => {
5104                        return Err("Unsupported segment type for split".to_string());
5105                    }
5106                };
5107
5108                let (_edit_source_delta, edit_scene_graph_delta) = frontend
5109                    .edit_segments(
5110                        ctx,
5111                        version,
5112                        sketch_id,
5113                        vec![ExistingSegmentCtor {
5114                            id: *segment_id,
5115                            ctor: edited_ctor,
5116                        }],
5117                    )
5118                    .await
5119                    .map_err(|e| format!("Failed to edit segment: {}", e.error.message()))?;
5120                // Track invalidates_ids from edit_segments call
5121                invalidates_ids = invalidates_ids || edit_scene_graph_delta.invalidates_ids;
5122
5123                // Get left endpoint ID from edited segment
5124                let edited_segment = edit_scene_graph_delta
5125                    .new_graph
5126                    .objects
5127                    .iter()
5128                    .find(|obj| obj.id == *segment_id)
5129                    .ok_or_else(|| format!("Failed to find edited segment {}", segment_id.0))?;
5130
5131                let left_side_endpoint_point_id = match &edited_segment.kind {
5132                    crate::frontend::api::ObjectKind::Segment { segment } => match segment {
5133                        crate::frontend::sketch::Segment::Line(line) => line.end,
5134                        crate::frontend::sketch::Segment::Arc(arc) => arc.end,
5135                        _ => {
5136                            return Err("Edited segment is not a Line or Arc".to_string());
5137                        }
5138                    },
5139                    _ => {
5140                        return Err("Edited segment is not a segment".to_string());
5141                    }
5142                };
5143
5144                // Step 5: Prepare constraints for batch
5145                let mut batch_constraints = Vec::new();
5146
5147                // Left constraint
5148                let left_intersecting_seg_id = match &**left_side {
5149                    TrimTermination::Intersection {
5150                        intersecting_seg_id, ..
5151                    }
5152                    | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5153                        intersecting_seg_id, ..
5154                    } => *intersecting_seg_id,
5155                    _ => {
5156                        return Err("Left side is not an intersection or coincident".to_string());
5157                    }
5158                };
5159                let left_coincident_segments = match &**left_side {
5160                    TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5161                        other_segment_point_id,
5162                        ..
5163                    } => {
5164                        vec![left_side_endpoint_point_id.into(), (*other_segment_point_id).into()]
5165                    }
5166                    _ => {
5167                        vec![left_side_endpoint_point_id.into(), left_intersecting_seg_id.into()]
5168                    }
5169                };
5170                batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
5171                    segments: left_coincident_segments,
5172                }));
5173
5174                // Right constraint - need to check if intersection is at endpoint
5175                let right_intersecting_seg_id = match &**right_side {
5176                    TrimTermination::Intersection {
5177                        intersecting_seg_id, ..
5178                    }
5179                    | TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5180                        intersecting_seg_id, ..
5181                    } => *intersecting_seg_id,
5182                    _ => {
5183                        return Err("Right side is not an intersection or coincident".to_string());
5184                    }
5185                };
5186
5187                let mut intersection_point_id: Option<ObjectId> = None;
5188                if matches!(&**right_side, TrimTermination::Intersection { .. }) {
5189                    let intersecting_seg = edit_scene_graph_delta
5190                        .new_graph
5191                        .objects
5192                        .iter()
5193                        .find(|obj| obj.id == right_intersecting_seg_id);
5194
5195                    if let Some(seg) = intersecting_seg {
5196                        let endpoint_epsilon = 1e-3; // In current trim unit
5197                        let right_trim_coords_value = *right_trim_coords;
5198
5199                        if let crate::frontend::api::ObjectKind::Segment { segment } = &seg.kind {
5200                            match segment {
5201                                crate::frontend::sketch::Segment::Line(_) => {
5202                                    if let (Some(start_coords), Some(end_coords)) = (
5203                                        crate::frontend::trim::get_position_coords_for_line(
5204                                            seg,
5205                                            crate::frontend::trim::LineEndpoint::Start,
5206                                            &edit_scene_graph_delta.new_graph.objects,
5207                                            default_unit,
5208                                        ),
5209                                        crate::frontend::trim::get_position_coords_for_line(
5210                                            seg,
5211                                            crate::frontend::trim::LineEndpoint::End,
5212                                            &edit_scene_graph_delta.new_graph.objects,
5213                                            default_unit,
5214                                        ),
5215                                    ) {
5216                                        let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
5217                                            * (right_trim_coords_value.x - start_coords.x)
5218                                            + (right_trim_coords_value.y - start_coords.y)
5219                                                * (right_trim_coords_value.y - start_coords.y))
5220                                            .sqrt();
5221                                        if dist_to_start < endpoint_epsilon {
5222                                            if let crate::frontend::sketch::Segment::Line(line) = segment {
5223                                                intersection_point_id = Some(line.start);
5224                                            }
5225                                        } else {
5226                                            let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
5227                                                * (right_trim_coords_value.x - end_coords.x)
5228                                                + (right_trim_coords_value.y - end_coords.y)
5229                                                    * (right_trim_coords_value.y - end_coords.y))
5230                                                .sqrt();
5231                                            if dist_to_end < endpoint_epsilon
5232                                                && let crate::frontend::sketch::Segment::Line(line) = segment
5233                                            {
5234                                                intersection_point_id = Some(line.end);
5235                                            }
5236                                        }
5237                                    }
5238                                }
5239                                crate::frontend::sketch::Segment::Arc(_) => {
5240                                    if let (Some(start_coords), Some(end_coords)) = (
5241                                        crate::frontend::trim::get_position_coords_from_arc(
5242                                            seg,
5243                                            crate::frontend::trim::ArcPoint::Start,
5244                                            &edit_scene_graph_delta.new_graph.objects,
5245                                            default_unit,
5246                                        ),
5247                                        crate::frontend::trim::get_position_coords_from_arc(
5248                                            seg,
5249                                            crate::frontend::trim::ArcPoint::End,
5250                                            &edit_scene_graph_delta.new_graph.objects,
5251                                            default_unit,
5252                                        ),
5253                                    ) {
5254                                        let dist_to_start = ((right_trim_coords_value.x - start_coords.x)
5255                                            * (right_trim_coords_value.x - start_coords.x)
5256                                            + (right_trim_coords_value.y - start_coords.y)
5257                                                * (right_trim_coords_value.y - start_coords.y))
5258                                            .sqrt();
5259                                        if dist_to_start < endpoint_epsilon {
5260                                            if let crate::frontend::sketch::Segment::Arc(arc) = segment {
5261                                                intersection_point_id = Some(arc.start);
5262                                            }
5263                                        } else {
5264                                            let dist_to_end = ((right_trim_coords_value.x - end_coords.x)
5265                                                * (right_trim_coords_value.x - end_coords.x)
5266                                                + (right_trim_coords_value.y - end_coords.y)
5267                                                    * (right_trim_coords_value.y - end_coords.y))
5268                                                .sqrt();
5269                                            if dist_to_end < endpoint_epsilon
5270                                                && let crate::frontend::sketch::Segment::Arc(arc) = segment
5271                                            {
5272                                                intersection_point_id = Some(arc.end);
5273                                            }
5274                                        }
5275                                    }
5276                                }
5277                                _ => {}
5278                            }
5279                        }
5280                    }
5281                }
5282
5283                let right_coincident_segments = if let Some(point_id) = intersection_point_id {
5284                    vec![new_segment_start_point_id.into(), point_id.into()]
5285                } else if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5286                    other_segment_point_id,
5287                    ..
5288                } = &**right_side
5289                {
5290                    vec![new_segment_start_point_id.into(), (*other_segment_point_id).into()]
5291                } else {
5292                    vec![new_segment_start_point_id.into(), right_intersecting_seg_id.into()]
5293                };
5294                batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
5295                    segments: right_coincident_segments,
5296                }));
5297
5298                // Migrate constraints
5299                let mut points_constrained_to_new_segment_start = std::collections::HashSet::new();
5300                let mut points_constrained_to_new_segment_end = std::collections::HashSet::new();
5301
5302                if let TrimTermination::TrimSpawnSegmentCoincidentWithAnotherSegmentPoint {
5303                    other_segment_point_id,
5304                    ..
5305                } = &**right_side
5306                {
5307                    points_constrained_to_new_segment_start.insert(other_segment_point_id);
5308                }
5309
5310                for constraint_to_migrate in constraints_to_migrate.iter() {
5311                    if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::End
5312                        && constraint_to_migrate.is_point_point
5313                    {
5314                        points_constrained_to_new_segment_end.insert(constraint_to_migrate.other_entity_id);
5315                    }
5316                }
5317
5318                for constraint_to_migrate in constraints_to_migrate.iter() {
5319                    // Skip migrating point-segment constraints if the point is already constrained
5320                    if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment
5321                        && (points_constrained_to_new_segment_start.contains(&constraint_to_migrate.other_entity_id)
5322                            || points_constrained_to_new_segment_end.contains(&constraint_to_migrate.other_entity_id))
5323                    {
5324                        continue; // Skip redundant constraint
5325                    }
5326
5327                    let constraint_segments = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Segment {
5328                        vec![constraint_to_migrate.other_entity_id.into(), new_segment_id.into()]
5329                    } else {
5330                        let target_endpoint_id = if constraint_to_migrate.attach_to_endpoint == AttachToEndpoint::Start
5331                        {
5332                            new_segment_start_point_id
5333                        } else {
5334                            new_segment_end_point_id
5335                        };
5336                        vec![target_endpoint_id.into(), constraint_to_migrate.other_entity_id.into()]
5337                    };
5338                    batch_constraints.push(Constraint::Coincident(crate::frontend::sketch::Coincident {
5339                        segments: constraint_segments,
5340                    }));
5341                }
5342
5343                // Find distance constraints that reference both endpoints of the original segment
5344                let mut distance_constraints_to_re_add: Vec<(
5345                    crate::frontend::api::Number,
5346                    Option<crate::frontend::sketch::Point2d<crate::frontend::api::Number>>,
5347                    crate::frontend::sketch::ConstraintSource,
5348                )> = Vec::new();
5349                if let (Some(original_start_id), Some(original_end_id)) =
5350                    (original_segment_start_point_id, original_segment_end_point_id)
5351                {
5352                    for obj in &edit_scene_graph_delta.new_graph.objects {
5353                        let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5354                            continue;
5355                        };
5356
5357                        let Constraint::Distance(distance) = constraint else {
5358                            continue;
5359                        };
5360
5361                        let references_start = distance.contains_point(original_start_id);
5362                        let references_end = distance.contains_point(original_end_id);
5363
5364                        if references_start && references_end {
5365                            distance_constraints_to_re_add.push((
5366                                distance.distance,
5367                                distance.label_position.clone(),
5368                                distance.source.clone(),
5369                            ));
5370                        }
5371                    }
5372                }
5373
5374                // Re-add distance constraints
5375                if let Some(original_start_id) = original_segment_start_point_id {
5376                    for (distance_value, label_position, source) in distance_constraints_to_re_add {
5377                        batch_constraints.push(Constraint::Distance(crate::frontend::sketch::Distance {
5378                            points: vec![original_start_id.into(), new_segment_end_point_id.into()],
5379                            distance: distance_value,
5380                            label_position,
5381                            source,
5382                        }));
5383                    }
5384                }
5385
5386                // Migrate center point constraints for arcs
5387                if let Some(new_center_id) = new_segment_center_point_id {
5388                    for (constraint, original_center_id) in center_point_constraints_to_migrate {
5389                        let center_rewrite_map = std::collections::HashMap::from([(original_center_id, new_center_id)]);
5390                        if let Some(rewritten) = rewrite_constraint_with_map(&constraint, &center_rewrite_map)
5391                            && matches!(rewritten, Constraint::Coincident(_) | Constraint::Distance(_))
5392                        {
5393                            batch_constraints.push(rewritten);
5394                        }
5395                    }
5396                }
5397
5398                // Re-add angle constraints (Parallel, Perpendicular, Horizontal, Vertical)
5399                let mut angle_rewrite_map = std::collections::HashMap::from([(*segment_id, new_segment_id)]);
5400                if let Some(original_end_id) = original_segment_end_point_id {
5401                    angle_rewrite_map.insert(original_end_id, new_segment_end_point_id);
5402                }
5403                for obj in &edit_scene_graph_delta.new_graph.objects {
5404                    let crate::frontend::api::ObjectKind::Constraint { constraint } = &obj.kind else {
5405                        continue;
5406                    };
5407
5408                    // Keep this exhaustive so new constraints must declare
5409                    // whether split trim should migrate them to the new segment.
5410                    let should_migrate = match constraint {
5411                        Constraint::Parallel(parallel) => parallel.lines.contains(segment_id),
5412                        Constraint::Perpendicular(perpendicular) => perpendicular.lines.contains(segment_id),
5413                        Constraint::Horizontal(Horizontal::Line { line }) => line == segment_id,
5414                        Constraint::Horizontal(Horizontal::Points { points }) => original_segment_end_point_id
5415                            .is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
5416                        Constraint::Vertical(Vertical::Line { line }) => line == segment_id,
5417                        Constraint::Vertical(Vertical::Points { points }) => original_segment_end_point_id
5418                            .is_some_and(|end_id| points.contains(&ConstraintSegment::from(end_id))),
5419                        Constraint::Angle(_)
5420                        | Constraint::Coincident(_)
5421                        | Constraint::Diameter(_)
5422                        | Constraint::Distance(_)
5423                        | Constraint::EqualRadius(_)
5424                        | Constraint::Fixed(_)
5425                        | Constraint::HorizontalDistance(_)
5426                        | Constraint::LinesEqualLength(_)
5427                        | Constraint::Midpoint(_)
5428                        | Constraint::Radius(_)
5429                        | Constraint::Symmetric(_)
5430                        | Constraint::Tangent(_)
5431                        | Constraint::VerticalDistance(_) => false,
5432                    };
5433
5434                    if should_migrate
5435                        && let Some(migrated_constraint) = rewrite_constraint_with_map(constraint, &angle_rewrite_map)
5436                        && matches!(
5437                            migrated_constraint,
5438                            Constraint::Parallel(_)
5439                                | Constraint::Perpendicular(_)
5440                                | Constraint::Horizontal(_)
5441                                | Constraint::Vertical(_)
5442                        )
5443                    {
5444                        batch_constraints.push(migrated_constraint);
5445                    }
5446                }
5447
5448                // Step 6: Batch all remaining operations
5449                let constraint_object_ids: Vec<ObjectId> = constraints_to_delete.to_vec();
5450
5451                let batch_result = frontend
5452                    .batch_split_segment_operations(
5453                        ctx,
5454                        version,
5455                        sketch_id,
5456                        Vec::new(), // edit_segments already done
5457                        batch_constraints,
5458                        constraint_object_ids,
5459                        crate::frontend::sketch::NewSegmentInfo {
5460                            segment_id: new_segment_id,
5461                            start_point_id: new_segment_start_point_id,
5462                            end_point_id: new_segment_end_point_id,
5463                            center_point_id: new_segment_center_point_id,
5464                        },
5465                    )
5466                    .await
5467                    .map_err(|e| format!("Failed to batch split segment operations: {}", e.error.message()));
5468                // Track invalidates_ids from batch_split_segment_operations call
5469                if let Ok((_, ref batch_delta)) = batch_result {
5470                    invalidates_ids = invalidates_ids || batch_delta.invalidates_ids;
5471                }
5472                batch_result
5473            }
5474        };
5475
5476        match operation_result {
5477            Ok((source_delta, scene_graph_delta)) => {
5478                // Track invalidates_ids from each operation result
5479                invalidates_ids = invalidates_ids || scene_graph_delta.invalidates_ids;
5480                last_result = Some((source_delta, scene_graph_delta.clone()));
5481            }
5482            Err(e) => {
5483                crate::logln!("Error executing trim operation {}: {}", op_index, e);
5484                // Continue to next operation
5485            }
5486        }
5487
5488        op_index += consumed_ops;
5489    }
5490
5491    let (source_delta, mut scene_graph_delta) =
5492        last_result.ok_or_else(|| "No operations were executed successfully".to_string())?;
5493    // Set invalidates_ids if any operation invalidated IDs
5494    scene_graph_delta.invalidates_ids = invalidates_ids;
5495    Ok((source_delta, scene_graph_delta))
5496}