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