Skip to main content

kcl_lib/frontend/
trim.rs

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