Skip to main content

kcl_lib/frontend/
trim.rs

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