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