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