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