Skip to main content

kcl_lib/frontend/
trim.rs

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