Skip to main content

kcl_lib/frontend/
trim.rs

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