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