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