Skip to main content

kcl_lib/frontend/
trim.rs

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