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