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