Skip to main content

kcl_lib/frontend/
trim.rs

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