Skip to main content

kcl_lib/
frontend.rs

1use std::cell::Cell;
2use std::collections::HashMap;
3use std::collections::HashSet;
4use std::collections::VecDeque;
5use std::ops::ControlFlow;
6
7use indexmap::IndexMap;
8use kcl_api::Angle;
9use kcl_api::ArcCtor;
10use kcl_api::ArcDirection;
11use kcl_api::CapSource;
12use kcl_api::CircleCtor;
13use kcl_api::Coincident;
14use kcl_api::Constraint;
15use kcl_api::ConstraintSegment;
16use kcl_api::ControlPointSplineCtor;
17use kcl_api::Diameter;
18use kcl_api::Distance;
19use kcl_api::EqualRadius;
20use kcl_api::Expr;
21use kcl_api::FixedPoint;
22use kcl_api::Freedom;
23use kcl_api::Horizontal;
24use kcl_api::LineCtor;
25use kcl_api::LinesEqualLength;
26use kcl_api::Midpoint;
27use kcl_api::Number;
28use kcl_api::NumericSuffix;
29use kcl_api::Object;
30use kcl_api::ObjectId;
31use kcl_api::ObjectKind;
32use kcl_api::Parallel;
33use kcl_api::Perpendicular;
34use kcl_api::Plane;
35use kcl_api::Point2d;
36use kcl_api::PointCtor;
37use kcl_api::Radius;
38use kcl_api::Segment;
39use kcl_api::SegmentCtor;
40use kcl_api::SketchCtor;
41use kcl_api::SourceRef;
42use kcl_api::SourceRefRange;
43use kcl_api::Symmetric;
44use kcl_api::Tangent;
45use kcl_api::UnitLength;
46use kcl_api::Vertical;
47use kcl_api::WallSource;
48use kcl_error::CompilationIssue;
49use kcl_error::SourceRange;
50use serde::Serialize;
51use uuid::Uuid;
52
53use crate::ExecOutcome;
54use crate::ExecutorContext;
55use crate::KclError;
56use crate::KclErrorWithOutputs;
57use crate::KclValueView;
58use crate::Program;
59use crate::SegmentDragAnchor;
60use crate::collections::AhashIndexSet;
61use crate::execution::Artifact;
62use crate::execution::ArtifactGraph;
63use crate::execution::ArtifactId;
64use crate::execution::CapSubType;
65use crate::execution::CodeRef;
66use crate::execution::MockConfig;
67use crate::execution::SKETCH_BLOCK_PARAM_ON;
68use crate::execution::annotations::WarningLevel;
69use crate::execution::cache::SketchModeState;
70use crate::execution::cache::clear_mem_cache;
71use crate::execution::cache::read_old_memory;
72use crate::execution::cache::write_old_memory;
73use crate::execution::types::adjust_length;
74use crate::fmt::format_number_literal;
75use crate::front::CoincidentExt;
76use crate::front::DistanceExt;
77use crate::front::Error;
78use crate::front::ExecResult;
79use crate::front::ObjectKindExt;
80use crate::front::SegmentExt;
81use crate::frontend::api::FileId;
82use crate::frontend::api::ProjectId;
83use crate::frontend::api::RestoreSketchCheckpointOutcome;
84use crate::frontend::api::SceneGraph;
85use crate::frontend::api::SceneGraphDelta;
86use crate::frontend::api::SketchCheckpointId;
87use crate::frontend::api::SourceDelta;
88use crate::frontend::api::Version;
89use crate::frontend::modify::find_defined_names;
90use crate::frontend::modify::next_free_name;
91use crate::frontend::modify::next_free_name_with_padding;
92use crate::frontend::sketch::ConstraintLabelPositionEdit;
93use crate::frontend::sketch::ExistingSegmentCtor;
94use crate::frontend::sketch::SketchApi;
95use crate::id::IncIdGenerator;
96use crate::parsing::ast::types as ast;
97use crate::parsing::ast::types::BoxNode;
98use crate::parsing::ast::types::CallExpressionKw;
99use crate::parsing::ast::types::NodePathExt;
100use crate::std::constraints::LinesAtAngleKind;
101use crate::walk::NodeMut;
102use crate::walk::Visitable;
103use crate::walk::traverse::MutateBodyItem;
104use crate::walk::traverse::TraversalReturn;
105use crate::walk::traverse::Visitor;
106use crate::walk::traverse::dfs_mut;
107
108pub(crate) mod api;
109pub(crate) mod modify;
110pub(crate) mod sketch;
111
112pub const MAX_SKETCH_CHECKPOINTS: usize = 100;
113
114#[derive(Debug, Clone)]
115struct SketchCheckpoint {
116    id: SketchCheckpointId,
117    source: SourceDelta,
118    program: Program,
119    scene_graph: SceneGraph,
120    exec_outcome: ExecOutcome,
121    point_freedom_cache: HashMap<ObjectId, Freedom>,
122    mock_memory: Option<SketchModeState>,
123}
124pub(crate) mod trim;
125
126struct ArcSizeConstraintParams {
127    points: Vec<ObjectId>,
128    function_name: &'static str,
129    value: f64,
130    units: NumericSuffix,
131    label_position: Option<Point2d<Number>>,
132    constraint_type_name: &'static str,
133}
134
135const POINT_FN: &str = "point";
136const POINT_AT_PARAM: &str = "at";
137const LINE_FN: &str = "line";
138const LINE_VARIABLE: &str = "line";
139const LINE_START_PARAM: &str = "start";
140const LINE_END_PARAM: &str = "end";
141const ARC_FN: &str = "arc";
142const ARC_VARIABLE: &str = "arc";
143const ARC_START_PARAM: &str = "start";
144const ARC_END_PARAM: &str = "end";
145const ARC_CENTER_PARAM: &str = "center";
146const ARC_DIRECTION_PARAM: &str = "direction";
147/// The name of the KCL std constant for a clockwise arc direction.
148const ARC_DIRECTION_CW_NAME: &str = "CW";
149const CIRCLE_FN: &str = "circle";
150const CIRCLE_VARIABLE: &str = "circle";
151const CIRCLE_START_PARAM: &str = "start";
152const CIRCLE_CENTER_PARAM: &str = "center";
153const CONTROL_POINT_SPLINE_FN: &str = "controlPointSpline";
154const CONTROL_POINT_SPLINE_POINTS_PARAM: &str = "points";
155const LABEL_POSITION_PARAM: &str = "labelPosition";
156
157const COINCIDENT_FN: &str = "coincident";
158const DIAMETER_FN: &str = "diameter";
159const DISTANCE_FN: &str = "distance";
160const FIXED_FN: &str = "fixed";
161const ANGLE_FN: &str = "angle";
162const ANGLE_DIMENSION_FN: &str = "angleDimension";
163const ANGLE_LINES_PARAM: &str = "lines";
164const ANGLE_SECTOR_PARAM: &str = "sector";
165const ANGLE_INVERSE_PARAM: &str = "inverse";
166const HORIZONTAL_DISTANCE_FN: &str = "horizontalDistance";
167const VERTICAL_DISTANCE_FN: &str = "verticalDistance";
168const EQUAL_LENGTH_FN: &str = "equalLength";
169const EQUAL_RADIUS_FN: &str = "equalRadius";
170const HORIZONTAL_FN: &str = "horizontal";
171const MIDPOINT_FN: &str = "midpoint";
172const MIDPOINT_POINT_PARAM: &str = "point";
173const RADIUS_FN: &str = "radius";
174const SYMMETRIC_FN: &str = "symmetric";
175const SYMMETRIC_AXIS_PARAM: &str = "axis";
176const TANGENT_FN: &str = "tangent";
177const VERTICAL_FN: &str = "vertical";
178
179const LINE_PROPERTY_START: &str = "start";
180const LINE_PROPERTY_END: &str = "end";
181
182const ARC_PROPERTY_START: &str = "start";
183const ARC_PROPERTY_END: &str = "end";
184const ARC_PROPERTY_CENTER: &str = "center";
185const CIRCLE_PROPERTY_START: &str = "start";
186const CIRCLE_PROPERTY_CENTER: &str = "center";
187const CONTROL_POINT_SPLINE_PROPERTY_CONTROLS: &str = "controls";
188const CONTROL_POINT_SPLINE_PROPERTY_EDGES: &str = "edges";
189
190const CONSTRUCTION_PARAM: &str = "construction";
191
192#[derive(Debug, Clone, Copy)]
193enum EditDeleteKind {
194    Edit,
195    DeleteNonSketch,
196}
197
198/// Options that control how an edit is re-executed and written back.
199struct ExecuteAfterEditOptions {
200    segment_ids_edited: AhashIndexSet<ObjectId>,
201    edit_kind: EditDeleteKind,
202    commit_solved_initial_guesses: bool,
203}
204
205impl EditDeleteKind {
206    /// Returns true if this edit is any type of deletion.
207    fn is_delete(&self) -> bool {
208        match self {
209            EditDeleteKind::Edit => false,
210            EditDeleteKind::DeleteNonSketch => true,
211        }
212    }
213
214    fn to_change_kind(self) -> ChangeKind {
215        match self {
216            EditDeleteKind::Edit => ChangeKind::Edit,
217            EditDeleteKind::DeleteNonSketch => ChangeKind::Delete,
218        }
219    }
220}
221
222#[derive(Debug, Clone, Copy)]
223enum ChangeKind {
224    Add,
225    Edit,
226    Delete,
227    None,
228}
229
230#[derive(Debug, Clone, Serialize, ts_rs::TS)]
231#[ts(export, export_to = "FrontendApi.ts")]
232#[serde(tag = "type")]
233pub enum SetProgramOutcome {
234    #[serde(rename_all = "camelCase")]
235    Success {
236        scene_graph: Box<SceneGraph>,
237        exec_outcome: Box<ExecOutcome>,
238        checkpoint_id: Option<SketchCheckpointId>,
239    },
240    #[serde(rename_all = "camelCase")]
241    ExecFailure { error: Box<KclErrorWithOutputs> },
242}
243
244/// Options for a sketch segment edit that participates in drag solving.
245pub struct EditSegmentsOptions {
246    /// Narrows which edited scene objects receive temporary fixed constraints.
247    ///
248    /// `None` keeps the default of anchoring every edited segment. `Some(vec![])`
249    /// disables those fixed constraints, which is useful for semantic edits such
250    /// as toggling construction state.
251    pub anchor_segment_ids: Option<Vec<ObjectId>>,
252    /// Hidden fixed cursor points that the referenced segment bodies must pass
253    /// through during solve.
254    pub drag_anchors: Vec<SegmentDragAnchor>,
255    /// Constraint label positions to write in the same AST transaction as the
256    /// segment edits. Label positions do not participate in solving.
257    pub constraint_label_edits: Vec<ConstraintLabelPositionEdit>,
258    /// Whether solver-updated initial guesses should be written back to KCL.
259    pub commit_solved_initial_guesses: bool,
260}
261
262/// Options for a distance-constraint label edit during sketch dragging.
263pub struct EditDistanceConstraintLabelPositionOptions {
264    /// Edited scene objects to keep anchored while previewing the label edit.
265    pub anchor_segment_ids: Vec<ObjectId>,
266    /// Whether solver-updated initial guesses should be written back to KCL.
267    pub commit_solved_initial_guesses: bool,
268}
269
270/// Options for editing a constraint during sketch dragging.
271pub struct EditConstraintOptions {
272    /// Whether solver-updated initial guesses should be written back to KCL.
273    pub commit_solved_initial_guesses: bool,
274}
275
276#[derive(Debug, Clone)]
277struct SolidAstReference {
278    variable_name: String,
279    output_index: Option<usize>,
280}
281
282#[derive(Debug, Clone)]
283pub struct FrontendState {
284    program: Program,
285    scene_graph: SceneGraph,
286    /// Lightweight map from engine solid IDs to their latest KCL references.
287    solid_references: HashMap<Uuid, SolidAstReference>,
288    /// Stores the last known freedom value for each point object.
289    /// This allows us to preserve freedom values when freedom analysis isn't run.
290    point_freedom_cache: HashMap<ObjectId, Freedom>,
291    /// One-shot drag anchors for the next segment edit. These ids define which
292    /// edited points/segments become temporary fixed constraints during solve.
293    next_drag_anchor_segment_ids: Option<AhashIndexSet<ObjectId>>,
294    /// One-shot segment-body drag anchors for the next segment edit. These add
295    /// a temporary solver point on the dragged segment that follows the cursor.
296    next_segment_drag_anchors: Option<Vec<SegmentDragAnchor>>,
297    /// One-shot constraint label edits to apply with the next segment edit.
298    next_constraint_label_edits: Option<Vec<ConstraintLabelPositionEdit>>,
299    /// One-shot override for whether the next edit commits solver-updated
300    /// initial guesses back into KCL. Drag previews keep this off so only the
301    /// explicit drag edit feeds the next solve.
302    next_edit_commits_solver_solutions: Option<bool>,
303    sketch_checkpoints: VecDeque<SketchCheckpoint>,
304    sketch_checkpoint_id_gen: IncIdGenerator<u64>,
305}
306
307impl Default for FrontendState {
308    fn default() -> Self {
309        Self::new()
310    }
311}
312
313impl FrontendState {
314    pub fn new() -> Self {
315        Self {
316            program: Program::empty(),
317            scene_graph: SceneGraph {
318                project: ProjectId(0),
319                file: FileId(0),
320                version: Version(0),
321                objects: Default::default(),
322                settings: Default::default(),
323                sketch_mode: Default::default(),
324            },
325            solid_references: HashMap::new(),
326            point_freedom_cache: HashMap::new(),
327            next_drag_anchor_segment_ids: None,
328            next_segment_drag_anchors: None,
329            next_constraint_label_edits: None,
330            next_edit_commits_solver_solutions: None,
331            sketch_checkpoints: VecDeque::new(),
332            sketch_checkpoint_id_gen: IncIdGenerator::new(1),
333        }
334    }
335
336    /// Get a reference to the scene graph
337    pub fn scene_graph(&self) -> &SceneGraph {
338        &self.scene_graph
339    }
340
341    pub fn default_length_unit(&self) -> UnitLength {
342        self.program
343            .meta_settings()
344            .ok()
345            .flatten()
346            .map(|settings| settings.default_length_units)
347            .unwrap_or(UnitLength::Millimeters)
348    }
349
350    pub async fn create_sketch_checkpoint(&mut self, exec_outcome: ExecOutcome) -> api::Result<SketchCheckpointId> {
351        let checkpoint_id = SketchCheckpointId::new(self.sketch_checkpoint_id_gen.next_id());
352
353        let checkpoint = SketchCheckpoint {
354            id: checkpoint_id,
355            source: SourceDelta {
356                text: source_from_ast(&self.program.ast),
357            },
358            program: self.program.clone(),
359            scene_graph: self.scene_graph.clone(),
360            exec_outcome,
361            point_freedom_cache: self.point_freedom_cache.clone(),
362            mock_memory: read_old_memory().await,
363        };
364
365        self.sketch_checkpoints.push_back(checkpoint);
366        while self.sketch_checkpoints.len() > MAX_SKETCH_CHECKPOINTS {
367            self.sketch_checkpoints.pop_front();
368        }
369
370        Ok(checkpoint_id)
371    }
372
373    /// Edit sketch segments with optional drag-solve overrides.
374    ///
375    /// Drag anchors add hidden fixed cursor points and constrain the referenced
376    /// segment bodies to pass through them, which lets body drags use the same
377    /// anchor model without pinning all child points. Preview callers disable
378    /// solver writeback so solved geometry can be returned without feeding every
379    /// solver value back into KCL.
380    pub async fn edit_segments_with_options(
381        &mut self,
382        ctx: &ExecutorContext,
383        version: Version,
384        sketch: ObjectId,
385        segments: Vec<ExistingSegmentCtor>,
386        options: EditSegmentsOptions,
387    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
388        let previous_anchor_ids = options.anchor_segment_ids.map(|anchor_ids| {
389            self.next_drag_anchor_segment_ids
390                .replace(anchor_ids.into_iter().collect())
391        });
392        let previous_drag_anchors = self.next_segment_drag_anchors.replace(options.drag_anchors);
393        let previous_constraint_label_edits = self.next_constraint_label_edits.replace(options.constraint_label_edits);
394        let previous_commit_mode = self
395            .next_edit_commits_solver_solutions
396            .replace(options.commit_solved_initial_guesses);
397        let result = SketchApi::edit_segments(self, ctx, version, sketch, segments).await;
398        if let Some(previous_anchor_ids) = previous_anchor_ids {
399            self.next_drag_anchor_segment_ids = previous_anchor_ids;
400        }
401        self.next_segment_drag_anchors = previous_drag_anchors;
402        self.next_constraint_label_edits = previous_constraint_label_edits;
403        self.next_edit_commits_solver_solutions = previous_commit_mode;
404        result
405    }
406
407    /// Edit a distance-constraint label position with optional solver writeback.
408    ///
409    /// Drag previews set `commit_solved_initial_guesses` to false so label
410    /// placement can be previewed against solved geometry without advancing
411    /// persistent KCL state until drag completion.
412    pub async fn edit_distance_constraint_label_position_with_options(
413        &mut self,
414        ctx: &ExecutorContext,
415        version: Version,
416        sketch: ObjectId,
417        constraint_id: ObjectId,
418        label_position: Point2d<Number>,
419        options: EditDistanceConstraintLabelPositionOptions,
420    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
421        let previous_commit_mode = self
422            .next_edit_commits_solver_solutions
423            .replace(options.commit_solved_initial_guesses);
424        let result = SketchApi::edit_distance_constraint_label_position(
425            self,
426            ctx,
427            version,
428            sketch,
429            constraint_id,
430            label_position,
431            options.anchor_segment_ids,
432        )
433        .await;
434        self.next_edit_commits_solver_solutions = previous_commit_mode;
435        result
436    }
437
438    /// Edit a distance constraint with optional solver writeback.
439    pub async fn edit_distance_constraint_with_options(
440        &mut self,
441        ctx: &ExecutorContext,
442        version: Version,
443        sketch: ObjectId,
444        constraint_id: ObjectId,
445        constraint: Constraint,
446        options: EditConstraintOptions,
447    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
448        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, constraint, options)
449            .await
450    }
451
452    /// Edit an angle constraint with optional solver writeback.
453    pub async fn edit_angle_constraint_with_options(
454        &mut self,
455        ctx: &ExecutorContext,
456        version: Version,
457        sketch: ObjectId,
458        constraint_id: ObjectId,
459        angle: Angle,
460        options: EditConstraintOptions,
461    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
462        self.edit_constraint_with_options(ctx, version, sketch, constraint_id, Constraint::Angle(angle), options)
463            .await
464    }
465
466    /// Edit an angle or distance-family constraint with optional solver writeback.
467    pub async fn edit_constraint_with_options(
468        &mut self,
469        ctx: &ExecutorContext,
470        _version: Version,
471        sketch: ObjectId,
472        constraint_id: ObjectId,
473        constraint: Constraint,
474        options: EditConstraintOptions,
475    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
476        // TODO: Check version.
477        let sketch_block_ref =
478            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
479
480        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
481            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
482        })?;
483
484        let mut new_ast = self.program.ast.clone();
485        let command = match &object.kind {
486            ObjectKind::Constraint {
487                constraint:
488                    Constraint::Distance(_) | Constraint::HorizontalDistance(_) | Constraint::VerticalDistance(_),
489            } => {
490                let (function_name, distance) = match &constraint {
491                    Constraint::Distance(distance) => (DISTANCE_FN, distance),
492                    Constraint::HorizontalDistance(distance) => (HORIZONTAL_DISTANCE_FN, distance),
493                    Constraint::VerticalDistance(distance) => (VERTICAL_DISTANCE_FN, distance),
494                    _ => {
495                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
496                            "A distance constraint can only be replaced by another distance constraint".to_owned(),
497                        )));
498                    }
499                };
500                let (call, value) = self
501                    .distance_constraint_ast_parts(function_name, distance, &mut new_ast)
502                    .map_err(KclErrorWithOutputs::no_outputs)?;
503                AstMutateCommand::EditDistanceConstraint { call, value }
504            }
505            ObjectKind::Constraint {
506                constraint: Constraint::Angle(_),
507            } => {
508                let Constraint::Angle(angle) = &constraint else {
509                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
510                        "An angle constraint can only be replaced by another angle constraint".to_owned(),
511                    )));
512                };
513                let (call, value) = self
514                    .angle_constraint_ast_parts(angle, &mut new_ast)
515                    .map_err(KclErrorWithOutputs::no_outputs)?;
516                AstMutateCommand::EditAngleConstraint { call, value }
517            }
518            ObjectKind::Constraint { .. } => {
519                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
520                    "Editing {} is not supported",
521                    object.kind.human_friendly_kind_with_article(),
522                ))));
523            }
524            _ => {
525                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
526                    "Object is not a constraint: {constraint_id:?}"
527                ))));
528            }
529        };
530
531        self.mutate_ast(&mut new_ast, constraint_id, command)
532            .map_err(KclErrorWithOutputs::no_outputs)?;
533
534        self.execute_after_edit(
535            ctx,
536            sketch,
537            sketch_block_ref,
538            &mut new_ast,
539            ExecuteAfterEditOptions {
540                segment_ids_edited: Default::default(),
541                edit_kind: EditDeleteKind::Edit,
542                commit_solved_initial_guesses: options.commit_solved_initial_guesses,
543            },
544        )
545        .await
546    }
547
548    pub async fn restore_sketch_checkpoint(
549        &mut self,
550        checkpoint_id: SketchCheckpointId,
551    ) -> api::Result<RestoreSketchCheckpointOutcome> {
552        let checkpoint = self
553            .sketch_checkpoints
554            .iter()
555            .find(|checkpoint| checkpoint.id == checkpoint_id)
556            .cloned()
557            .ok_or_else(|| Error {
558                msg: format!("Sketch checkpoint not found: {checkpoint_id:?}"),
559            })?;
560
561        self.program = checkpoint.program;
562        self.scene_graph = checkpoint.scene_graph.clone();
563        self.solid_references = solid_references_from_variables(&self.program.ast, &checkpoint.exec_outcome.variables);
564        self.point_freedom_cache = checkpoint.point_freedom_cache;
565        self.next_drag_anchor_segment_ids = None;
566        self.next_segment_drag_anchors = None;
567        self.next_constraint_label_edits = None;
568        self.next_edit_commits_solver_solutions = None;
569
570        if let Some(mock_memory) = checkpoint.mock_memory {
571            write_old_memory(mock_memory).await;
572        } else {
573            clear_mem_cache().await;
574        }
575
576        Ok(RestoreSketchCheckpointOutcome {
577            source_delta: checkpoint.source,
578            scene_graph_delta: SceneGraphDelta {
579                new_graph: self.scene_graph_for_ui(),
580                new_objects: Vec::new(),
581                invalidates_ids: true,
582                exec_outcome: checkpoint.exec_outcome,
583            },
584        })
585    }
586
587    pub fn clear_sketch_checkpoints(&mut self) {
588        self.sketch_checkpoints.clear();
589    }
590    fn scene_graph_for_ui(&self) -> SceneGraph {
591        let has_control_point_splines = self.scene_graph.objects.iter().any(|object| {
592            matches!(
593                object.kind,
594                ObjectKind::Segment {
595                    segment: Segment::ControlPointSpline(_)
596                }
597            )
598        });
599
600        if !has_control_point_splines {
601            return self.scene_graph.clone();
602        }
603
604        let hidden_constraint_ids = self
605            .scene_graph
606            .objects
607            .iter()
608            .filter_map(|object| match &object.kind {
609                ObjectKind::Constraint {
610                    constraint: Constraint::Coincident(coincident),
611                } if coincident_is_internal_to_same_control_point_spline(coincident, &self.scene_graph) => {
612                    Some(object.id)
613                }
614                _ => None,
615            })
616            .collect::<HashSet<_>>();
617
618        if hidden_constraint_ids.is_empty() {
619            return self.scene_graph.clone();
620        }
621
622        let mut scene_graph = self.scene_graph.clone();
623        for object in &mut scene_graph.objects {
624            match &mut object.kind {
625                ObjectKind::Constraint { .. } if hidden_constraint_ids.contains(&object.id) => {
626                    object.kind = ObjectKind::Nil;
627                }
628                ObjectKind::Sketch(sketch) => {
629                    sketch
630                        .constraints
631                        .retain(|constraint_id| !hidden_constraint_ids.contains(constraint_id));
632                }
633                _ => {}
634            }
635        }
636
637        scene_graph
638    }
639}
640
641fn coincident_is_internal_to_same_control_point_spline(coincident: &Coincident, scene_graph: &SceneGraph) -> bool {
642    let mut first_owner_id = None;
643    for segment_id in coincident.segment_ids() {
644        let Some(owner_id) = owning_control_point_spline_id(segment_id, scene_graph) else {
645            return false;
646        };
647
648        match first_owner_id {
649            Some(first_owner_id) if first_owner_id != owner_id => return false,
650            Some(_) => {}
651            None => first_owner_id = Some(owner_id),
652        }
653    }
654
655    first_owner_id.is_some()
656}
657
658fn owning_control_point_spline_id(segment_id: ObjectId, scene_graph: &SceneGraph) -> Option<ObjectId> {
659    let object = scene_graph.objects.get(segment_id.0)?;
660    let ObjectKind::Segment { segment } = &object.kind else {
661        return None;
662    };
663
664    match segment {
665        Segment::ControlPointSpline(_) => Some(segment_id),
666        Segment::Point(point) => point
667            .owner
668            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
669        Segment::Line(line) => line
670            .owner
671            .filter(|owner_id| matches_control_point_spline_owner(*owner_id, scene_graph)),
672        _ => None,
673    }
674}
675
676fn matches_control_point_spline_owner(owner_id: ObjectId, scene_graph: &SceneGraph) -> bool {
677    matches!(
678        scene_graph.objects.get(owner_id.0).map(|object| &object.kind),
679        Some(ObjectKind::Segment {
680            segment: Segment::ControlPointSpline(_)
681        })
682    )
683}
684
685fn ensure_control_point_spline_experimental_features(program: &Program) -> Result<Program, KclError> {
686    let experimental_features_allowed = program
687        .meta_settings()
688        .ok()
689        .flatten()
690        .map(|settings| settings.experimental_features == WarningLevel::Allow)
691        .unwrap_or(false);
692    if experimental_features_allowed {
693        return Ok(program.clone());
694    }
695
696    program.change_experimental_features(Some(WarningLevel::Allow))
697}
698
699impl SketchApi for FrontendState {
700    async fn execute_mock(
701        &mut self,
702        ctx: &ExecutorContext,
703        _version: Version,
704        sketch: ObjectId,
705    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
706        let sketch_block_ref =
707            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
708
709        let mut truncated_program = self.program.clone();
710        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
711            .map_err(KclErrorWithOutputs::no_outputs)?;
712
713        // Execute.
714        let outcome = ctx
715            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
716            .await?;
717        let new_source = source_from_ast(&self.program.ast);
718        let src_delta = SourceDelta { text: new_source };
719        // MockConfig::default() has freedom_analysis: true
720        let outcome = self.update_state_after_exec(outcome, true);
721        let scene_graph_delta = SceneGraphDelta {
722            new_graph: self.scene_graph.clone(),
723            new_objects: Default::default(),
724            invalidates_ids: false,
725            exec_outcome: outcome,
726        };
727        Ok((src_delta, scene_graph_delta))
728    }
729
730    async fn new_sketch(
731        &mut self,
732        ctx: &ExecutorContext,
733        _project: ProjectId,
734        _file: FileId,
735        _version: Version,
736        args: SketchCtor,
737    ) -> ExecResult<(SourceDelta, SceneGraphDelta, ObjectId)> {
738        // TODO: Check version.
739
740        let mut new_ast = self.program.ast.clone();
741        // Create updated KCL source from args.
742        let mut plane_ast = sketch_on_ast_expr(&mut new_ast, &self.scene_graph, &self.solid_references, &args.on)
743            .map_err(KclErrorWithOutputs::no_outputs)?;
744        let mut defined_names = find_defined_names(&new_ast);
745        let is_face_of_expr = matches!(
746            &plane_ast,
747            ast::Expr::CallExpressionKw(call) if call.callee.name.name == "faceOf"
748        );
749        if is_face_of_expr {
750            let face_name = next_free_name_with_padding("face", &defined_names)
751                .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
752            let face_decl = ast::VariableDeclaration::new(
753                ast::VariableDeclarator::new(&face_name, plane_ast),
754                ast::ItemVisibility::Default,
755                ast::VariableKind::Const,
756            );
757            new_ast
758                .body
759                .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
760                    face_decl,
761                ))));
762            defined_names.insert(face_name.clone());
763            plane_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(&face_name)));
764        }
765        let sketch_ast = ast::SketchBlock {
766            arguments: vec![ast::LabeledArg {
767                label: Some(ast::Identifier::new(SKETCH_BLOCK_PARAM_ON)),
768                arg: plane_ast,
769            }],
770            body: Default::default(),
771            is_being_edited: false,
772            non_code_meta: Default::default(),
773            digest: None,
774        };
775        // Add a sketch block as a variable declaration directly, avoiding
776        // source-range mutation on a no-src node.
777        let sketch_name = next_free_name_with_padding("sketch", &defined_names)
778            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
779        let sketch_decl = ast::VariableDeclaration::new(
780            ast::VariableDeclarator::new(
781                &sketch_name,
782                ast::Expr::SketchBlock(BoxNode::new(ast::Node::no_src(sketch_ast))),
783            ),
784            ast::ItemVisibility::Default,
785            ast::VariableKind::Const,
786        );
787        new_ast
788            .body
789            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
790                sketch_decl,
791            ))));
792        // Convert to string source to create real source ranges.
793        let new_source = source_from_ast(&new_ast);
794        // Parse the new source.
795        let new_program = parse_frontend_mutation_source(
796            &new_source,
797            "Error parsing KCL source after adding sketch",
798            "No AST produced after adding sketch",
799        )?;
800
801        // Make sure to only set this if there are no errors.
802        self.program = new_program.clone();
803
804        // We need to do an engine execute so that the plane object gets created
805        // and is cached.
806        let outcome = ctx.run_with_caching(new_program.clone()).await?;
807        let freedom_analysis_ran = true;
808
809        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
810
811        let Some(sketch_id) = self
812            .scene_graph
813            .objects
814            .iter()
815            .filter_map(|object| match object.kind {
816                ObjectKind::Sketch(_) => Some(object.id),
817                _ => None,
818            })
819            .max_by_key(|id| id.0)
820        else {
821            return Err(KclErrorWithOutputs::from_error_outcome(
822                KclError::refactor("No objects in scene graph after adding sketch".to_owned()),
823                outcome,
824            ));
825        };
826        // Store the object in the scene.
827        self.scene_graph.sketch_mode = Some(sketch_id);
828
829        let src_delta = SourceDelta { text: new_source };
830        let scene_graph_delta = SceneGraphDelta {
831            new_graph: self.scene_graph_for_ui(),
832            invalidates_ids: false,
833            new_objects: vec![sketch_id],
834            exec_outcome: outcome,
835        };
836        Ok((src_delta, scene_graph_delta, sketch_id))
837    }
838
839    async fn edit_sketch(
840        &mut self,
841        ctx: &ExecutorContext,
842        _project: ProjectId,
843        _file: FileId,
844        _version: Version,
845        sketch: ObjectId,
846    ) -> ExecResult<SceneGraphDelta> {
847        // TODO: Check version.
848
849        // Look up existing sketch.
850        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
851            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
852        })?;
853        let ObjectKind::Sketch(_) = &sketch_object.kind else {
854            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
855                "Object is not a sketch, it is {}",
856                sketch_object.kind.human_friendly_kind_with_article()
857            ))));
858        };
859        let sketch_block_ref = expect_single_node_ref(sketch_object).map_err(KclErrorWithOutputs::no_outputs)?;
860
861        // Enter sketch mode by setting the sketch_mode.
862        self.scene_graph.sketch_mode = Some(sketch);
863
864        // Truncate after the sketch block for mock execution.
865        let mut truncated_program = self.program.clone();
866        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::None)
867            .map_err(KclErrorWithOutputs::no_outputs)?;
868
869        // Execute in mock mode to ensure state is up to date. The caller will
870        // want freedom analysis to display segments correctly.
871        let outcome = ctx
872            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch))
873            .await?;
874
875        // MockConfig::default() has freedom_analysis: true
876        let outcome = self.update_state_after_exec(outcome, true);
877        let scene_graph_delta = SceneGraphDelta {
878            new_graph: self.scene_graph_for_ui(),
879            invalidates_ids: false,
880            new_objects: Vec::new(),
881            exec_outcome: outcome,
882        };
883        Ok(scene_graph_delta)
884    }
885
886    async fn exit_sketch(
887        &mut self,
888        ctx: &ExecutorContext,
889        _version: Version,
890        sketch: ObjectId,
891    ) -> ExecResult<SceneGraph> {
892        // TODO: Check version.
893        #[cfg(not(target_arch = "wasm32"))]
894        let _ = sketch;
895        #[cfg(target_arch = "wasm32")]
896        if self.scene_graph.sketch_mode != Some(sketch) {
897            web_sys::console::warn_1(
898                &format!(
899                    "WARNING: exit_sketch: current state's sketch mode ID doesn't match the given sketch ID; state={:#?}, given={sketch:?}",
900                    self.scene_graph.sketch_mode
901                )
902                .into(),
903            );
904        }
905        self.scene_graph.sketch_mode = None;
906
907        // Execute.
908        let outcome = ctx.run_with_caching(self.program.clone()).await?;
909
910        // exit_sketch doesn't run freedom analysis, just clears sketch_mode
911        self.update_state_after_exec(outcome, false);
912
913        Ok(self.scene_graph_for_ui())
914    }
915
916    async fn delete_sketch(
917        &mut self,
918        ctx: &ExecutorContext,
919        _version: Version,
920        sketch: ObjectId,
921    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
922        // TODO: Check version.
923
924        let mut new_ast = self.program.ast.clone();
925
926        // Look up existing sketch.
927        let sketch_id = sketch;
928        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
929            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
930        })?;
931        let ObjectKind::Sketch(_) = &sketch_object.kind else {
932            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
933                "Object is not a sketch, it is {}",
934                sketch_object.kind.human_friendly_kind_with_article(),
935            ))));
936        };
937
938        // Modify the AST to remove the sketch.
939        self.mutate_ast(&mut new_ast, sketch_id, AstMutateCommand::DeleteNode)
940            .map_err(KclErrorWithOutputs::no_outputs)?;
941
942        self.execute_after_delete_sketch(ctx, &mut new_ast).await
943    }
944
945    async fn add_segment(
946        &mut self,
947        ctx: &ExecutorContext,
948        _version: Version,
949        sketch: ObjectId,
950        segment: SegmentCtor,
951        _label: Option<String>,
952    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
953        // TODO: Check version.
954        match segment {
955            SegmentCtor::Point(ctor) => self.add_point(ctx, sketch, ctor).await,
956            SegmentCtor::Line(ctor) => self.add_line(ctx, sketch, ctor).await,
957            SegmentCtor::Arc(ctor) => self.add_arc(ctx, sketch, ctor).await,
958            SegmentCtor::Circle(ctor) => self.add_circle(ctx, sketch, ctor).await,
959            SegmentCtor::ControlPointSpline(ctor) => self.add_control_point_spline(ctx, sketch, ctor).await,
960        }
961    }
962
963    async fn edit_segments(
964        &mut self,
965        ctx: &ExecutorContext,
966        _version: Version,
967        sketch: ObjectId,
968        segments: Vec<ExistingSegmentCtor>,
969    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
970        // TODO: Check version.
971        let sketch_block_ref =
972            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
973
974        let mut new_ast = self.program.ast.clone();
975        let mut edited_segment_ids = AhashIndexSet::with_capacity_and_hasher(segments.len(), Default::default());
976        let mut invalidates_ids = false;
977
978        // edited_segment_ids still has to be the original segments (not final_edits), otherwise the owner segments
979        // are passed to `execute_after_edit` which changes the result of the solver, causing tests to fail.
980        for segment in &segments {
981            edited_segment_ids.insert(segment.id);
982            if let SegmentCtor::ControlPointSpline(new_ctor) = &segment.ctor
983                && let Some(existing_object) = self.scene_graph.objects.get(segment.id.0)
984                && let ObjectKind::Segment {
985                    segment: Segment::ControlPointSpline(existing_spline),
986                } = &existing_object.kind
987                && existing_spline.controls.len() != new_ctor.points.len()
988            {
989                invalidates_ids = true;
990            }
991        }
992        let drag_anchor_segment_ids = self
993            .next_drag_anchor_segment_ids
994            .take()
995            .unwrap_or_else(|| edited_segment_ids.clone());
996        let constraint_label_edits = self.next_constraint_label_edits.take().unwrap_or_default();
997        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
998
999        // Preprocess segments into a final_edits vector to handle if segments contains:
1000        // - edit start point of line1 (as SegmentCtor::Point)
1001        // - edit end point of line1 (as SegmentCtor::Point)
1002        //
1003        // This would result in only the end point to be updated because edit_point() clones line1's ctor from
1004        // scene_graph, but this is still the old ctor because self.scene_graph is only updated after the loop finishes.
1005        //
1006        // To fix this, and other cases when the same point is edited from multiple elements in the segments Vec
1007        // we apply all edits in order to final_edits in a way that owned point edits result in line edits,
1008        // so the above example would result in a single line1 edit:
1009        // - the first start point edit creates a new line edit entry in final_edits
1010        // - the second end point edit finds this line edit and mutates the end position only.
1011        //
1012        // The result is that segments are flattened into a single IndexMap of edits by their owners, later edits overriding earlier ones.
1013        let mut final_edits: IndexMap<ObjectId, SegmentCtor> = IndexMap::new();
1014
1015        for segment in segments {
1016            let segment_id = segment.id;
1017            match segment.ctor {
1018                SegmentCtor::Point(ctor) => {
1019                    // Find the owner, if any (point -> line / arc)
1020                    if let Some(segment_object) = self.scene_graph.objects.get(segment_id.0)
1021                        && let ObjectKind::Segment { segment } = &segment_object.kind
1022                        && let Segment::Point(point) = segment
1023                        && let Some(owner_id) = point.owner
1024                        && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1025                        && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1026                    {
1027                        match owner_segment {
1028                            Segment::Line(line) if line.start == segment_id || line.end == segment_id => {
1029                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1030                                    let SegmentCtor::Line(line_ctor) = existing else {
1031                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1032                                            "Internal: Expected line ctor for owner, but found {}",
1033                                            existing.human_friendly_kind_with_article()
1034                                        ))));
1035                                    };
1036                                    // Line owner is already in final_edits -> apply this point edit
1037                                    if line.start == segment_id {
1038                                        line_ctor.start = ctor.position;
1039                                    } else {
1040                                        line_ctor.end = ctor.position;
1041                                    }
1042                                } else if let SegmentCtor::Line(line_ctor) = &line.ctor {
1043                                    // Line owner is not in final_edits yet -> create it
1044                                    let mut line_ctor = line_ctor.clone();
1045                                    if line.start == segment_id {
1046                                        line_ctor.start = ctor.position;
1047                                    } else {
1048                                        line_ctor.end = ctor.position;
1049                                    }
1050                                    final_edits.insert(owner_id, SegmentCtor::Line(line_ctor));
1051                                } else {
1052                                    // This should never run..
1053                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1054                                        "Internal: Line does not have line ctor, but found {}",
1055                                        line.ctor.human_friendly_kind_with_article()
1056                                    ))));
1057                                }
1058                                continue;
1059                            }
1060                            Segment::Arc(arc)
1061                                if arc.start == segment_id || arc.end == segment_id || arc.center == segment_id =>
1062                            {
1063                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1064                                    let SegmentCtor::Arc(arc_ctor) = existing else {
1065                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1066                                            "Internal: Expected arc ctor for owner, but found {}",
1067                                            existing.human_friendly_kind_with_article()
1068                                        ))));
1069                                    };
1070                                    if arc.start == segment_id {
1071                                        arc_ctor.start = ctor.position;
1072                                    } else if arc.end == segment_id {
1073                                        arc_ctor.end = ctor.position;
1074                                    } else {
1075                                        arc_ctor.center = ctor.position;
1076                                    }
1077                                } else if let SegmentCtor::Arc(arc_ctor) = &arc.ctor {
1078                                    let mut arc_ctor = arc_ctor.clone();
1079                                    if arc.start == segment_id {
1080                                        arc_ctor.start = ctor.position;
1081                                    } else if arc.end == segment_id {
1082                                        arc_ctor.end = ctor.position;
1083                                    } else {
1084                                        arc_ctor.center = ctor.position;
1085                                    }
1086                                    final_edits.insert(owner_id, SegmentCtor::Arc(arc_ctor));
1087                                } else {
1088                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1089                                        "Internal: Arc does not have arc ctor, but found {}",
1090                                        arc.ctor.human_friendly_kind_with_article()
1091                                    ))));
1092                                }
1093                                continue;
1094                            }
1095                            Segment::Circle(circle) if circle.start == segment_id || circle.center == segment_id => {
1096                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1097                                    let SegmentCtor::Circle(circle_ctor) = existing else {
1098                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1099                                            "Internal: Expected circle ctor for owner, but found {}",
1100                                            existing.human_friendly_kind_with_article()
1101                                        ))));
1102                                    };
1103                                    if circle.start == segment_id {
1104                                        circle_ctor.start = ctor.position;
1105                                    } else {
1106                                        circle_ctor.center = ctor.position;
1107                                    }
1108                                } else if let SegmentCtor::Circle(circle_ctor) = &circle.ctor {
1109                                    let mut circle_ctor = circle_ctor.clone();
1110                                    if circle.start == segment_id {
1111                                        circle_ctor.start = ctor.position;
1112                                    } else {
1113                                        circle_ctor.center = ctor.position;
1114                                    }
1115                                    final_edits.insert(owner_id, SegmentCtor::Circle(circle_ctor));
1116                                } else {
1117                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1118                                        "Internal: Circle does not have circle ctor, but found {}",
1119                                        circle.ctor.human_friendly_kind_with_article()
1120                                    ))));
1121                                }
1122                                continue;
1123                            }
1124                            Segment::ControlPointSpline(spline) if spline.controls.contains(&segment_id) => {
1125                                let Some(control_index) =
1126                                    spline.controls.iter().position(|control_id| *control_id == segment_id)
1127                                else {
1128                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1129                                        "Internal: Point is not part of owner's controlPointSpline segment: point={segment_id:?}, spline={owner_id:?}"
1130                                    ))));
1131                                };
1132                                if let Some(existing) = final_edits.get_mut(&owner_id) {
1133                                    let SegmentCtor::ControlPointSpline(spline_ctor) = existing else {
1134                                        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1135                                            "Internal: Expected controlPointSpline ctor for owner, but found {}",
1136                                            existing.human_friendly_kind_with_article()
1137                                        ))));
1138                                    };
1139                                    spline_ctor.points[control_index] = ctor.position;
1140                                } else if let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor {
1141                                    let mut spline_ctor = spline_ctor.clone();
1142                                    spline_ctor.points[control_index] = ctor.position;
1143                                    final_edits.insert(owner_id, SegmentCtor::ControlPointSpline(spline_ctor));
1144                                } else {
1145                                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1146                                        "Internal: Control point spline does not have controlPointSpline ctor, but found {}",
1147                                        spline.ctor.human_friendly_kind_with_article()
1148                                    ))));
1149                                }
1150                                continue;
1151                            }
1152                            _ => {}
1153                        }
1154                    }
1155
1156                    // No owner, it's an individual point
1157                    final_edits.insert(segment_id, SegmentCtor::Point(ctor));
1158                }
1159                SegmentCtor::Line(ctor) => {
1160                    final_edits.insert(segment_id, SegmentCtor::Line(ctor));
1161                }
1162                SegmentCtor::Arc(ctor) => {
1163                    final_edits.insert(segment_id, SegmentCtor::Arc(ctor));
1164                }
1165                SegmentCtor::Circle(ctor) => {
1166                    final_edits.insert(segment_id, SegmentCtor::Circle(ctor));
1167                }
1168                SegmentCtor::ControlPointSpline(ctor) => {
1169                    final_edits.insert(segment_id, SegmentCtor::ControlPointSpline(ctor));
1170                }
1171            }
1172        }
1173
1174        for (segment_id, ctor) in final_edits {
1175            match ctor {
1176                SegmentCtor::Point(ctor) => self
1177                    .edit_point(&mut new_ast, sketch, segment_id, ctor)
1178                    .map_err(KclErrorWithOutputs::no_outputs)?,
1179                SegmentCtor::Line(ctor) => self
1180                    .edit_line(&mut new_ast, sketch, segment_id, ctor)
1181                    .map_err(KclErrorWithOutputs::no_outputs)?,
1182                SegmentCtor::Arc(ctor) => self
1183                    .edit_arc(&mut new_ast, sketch, segment_id, ctor)
1184                    .map_err(KclErrorWithOutputs::no_outputs)?,
1185                SegmentCtor::Circle(ctor) => self
1186                    .edit_circle(&mut new_ast, sketch, segment_id, ctor)
1187                    .map_err(KclErrorWithOutputs::no_outputs)?,
1188                SegmentCtor::ControlPointSpline(ctor) => self
1189                    .edit_control_point_spline(&mut new_ast, sketch, segment_id, ctor)
1190                    .map_err(KclErrorWithOutputs::no_outputs)?,
1191            }
1192        }
1193        for edit in constraint_label_edits {
1194            self.mutate_constraint_label_position(&mut new_ast, edit.constraint_id, edit.label_position)
1195                .map_err(KclErrorWithOutputs::no_outputs)?;
1196        }
1197        let (source_delta, mut scene_graph_delta) = self
1198            .execute_after_edit(
1199                ctx,
1200                sketch,
1201                sketch_block_ref,
1202                &mut new_ast,
1203                ExecuteAfterEditOptions {
1204                    segment_ids_edited: drag_anchor_segment_ids,
1205                    edit_kind: EditDeleteKind::Edit,
1206                    commit_solved_initial_guesses,
1207                },
1208            )
1209            .await?;
1210        if invalidates_ids {
1211            scene_graph_delta.invalidates_ids = true;
1212        }
1213        Ok((source_delta, scene_graph_delta))
1214    }
1215
1216    async fn delete_objects(
1217        &mut self,
1218        ctx: &ExecutorContext,
1219        _version: Version,
1220        sketch: ObjectId,
1221        constraint_ids: Vec<ObjectId>,
1222        segment_ids: Vec<ObjectId>,
1223    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1224        // TODO: Check version.
1225        let sketch_block_ref =
1226            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1227
1228        // Deduplicate IDs.
1229        let mut constraint_ids_set = constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1230        let segment_ids_set = segment_ids.into_iter().collect::<AhashIndexSet<_>>();
1231
1232        // If a point is owned by a Line/Arc, we want to delete the owner, which will
1233        // also delete the point, as well as other points that are owned by the owner.
1234        let mut resolved_segment_ids_to_delete = AhashIndexSet::default();
1235
1236        for segment_id in segment_ids_set.iter().copied() {
1237            let owner_id = self.scene_graph.objects.get(segment_id.0).and_then(|segment_object| {
1238                let ObjectKind::Segment { segment } = &segment_object.kind else {
1239                    return None;
1240                };
1241                match segment {
1242                    Segment::Point(point) => point.owner,
1243                    Segment::Line(line) => line.owner,
1244                    _ => None,
1245                }
1246            });
1247
1248            if let Some(owner_id) = owner_id
1249                && let Some(owner_object) = self.scene_graph.objects.get(owner_id.0)
1250                && let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind
1251                && matches!(
1252                    owner_segment,
1253                    Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) | Segment::ControlPointSpline(_)
1254                )
1255            {
1256                // segment is owned -> delete the owner
1257                resolved_segment_ids_to_delete.insert(owner_id);
1258            } else {
1259                // segment is not owned by anything -> can be deleted
1260                resolved_segment_ids_to_delete.insert(segment_id);
1261            }
1262        }
1263        let referenced_constraint_ids = self
1264            .find_referenced_constraints(sketch, &resolved_segment_ids_to_delete)
1265            .map_err(KclErrorWithOutputs::no_outputs)?;
1266
1267        let mut new_ast = self.program.ast.clone();
1268
1269        for constraint_id in referenced_constraint_ids {
1270            if constraint_ids_set.contains(&constraint_id) {
1271                continue;
1272            }
1273
1274            let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1275                KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Constraint not found: {constraint_id:?}")))
1276            })?;
1277            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
1278                return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1279                    "Object is not a constraint, it is {}",
1280                    constraint_object.kind.human_friendly_kind_with_article()
1281                ))));
1282            };
1283
1284            match constraint {
1285                Constraint::Coincident(coincident) => {
1286                    let remaining_segments =
1287                        self.remaining_constraint_segments(&coincident.segments, &resolved_segment_ids_to_delete);
1288
1289                    // If there are at least 2 segments left in the constraint: keep it, otherwise delete it.
1290                    if remaining_segments.len() >= 2 {
1291                        self.edit_coincident_constraint(&mut new_ast, constraint_id, remaining_segments)
1292                            .map_err(KclErrorWithOutputs::no_outputs)?;
1293                    } else {
1294                        constraint_ids_set.insert(constraint_id);
1295                    }
1296                }
1297                Constraint::EqualRadius(equal_radius) => {
1298                    let remaining_input = equal_radius
1299                        .input
1300                        .iter()
1301                        .copied()
1302                        .filter(|segment_id| {
1303                            !self.segment_will_be_deleted(*segment_id, &resolved_segment_ids_to_delete)
1304                        })
1305                        .collect::<Vec<_>>();
1306
1307                    if remaining_input.len() >= 2 {
1308                        self.edit_equal_radius_constraint(&mut new_ast, constraint_id, remaining_input)
1309                            .map_err(KclErrorWithOutputs::no_outputs)?;
1310                    } else {
1311                        constraint_ids_set.insert(constraint_id);
1312                    }
1313                }
1314                Constraint::LinesEqualLength(lines_equal_length) => {
1315                    let remaining_lines = lines_equal_length
1316                        .lines
1317                        .iter()
1318                        .copied()
1319                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1320                        .collect::<Vec<_>>();
1321
1322                    // Equal length constraint is only valid with at least 2 lines
1323                    if remaining_lines.len() >= 2 {
1324                        self.edit_equal_length_constraint(&mut new_ast, constraint_id, remaining_lines)
1325                            .map_err(KclErrorWithOutputs::no_outputs)?;
1326                    } else {
1327                        constraint_ids_set.insert(constraint_id);
1328                    }
1329                }
1330                Constraint::Parallel(parallel) => {
1331                    let remaining_lines = parallel
1332                        .lines
1333                        .iter()
1334                        .copied()
1335                        .filter(|line_id| !self.segment_will_be_deleted(*line_id, &resolved_segment_ids_to_delete))
1336                        .collect::<Vec<_>>();
1337
1338                    if remaining_lines.len() >= 2 {
1339                        self.edit_parallel_constraint(&mut new_ast, constraint_id, remaining_lines)
1340                            .map_err(KclErrorWithOutputs::no_outputs)?;
1341                    } else {
1342                        constraint_ids_set.insert(constraint_id);
1343                    }
1344                }
1345                Constraint::Horizontal(Horizontal::Points { points }) => {
1346                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1347
1348                    if remaining_points.len() >= 2 {
1349                        self.edit_horizontal_points_constraint(&mut new_ast, constraint_id, remaining_points)
1350                            .map_err(KclErrorWithOutputs::no_outputs)?;
1351                    } else {
1352                        constraint_ids_set.insert(constraint_id);
1353                    }
1354                }
1355                Constraint::Vertical(Vertical::Points { points }) => {
1356                    let remaining_points = self.remaining_constraint_segments(points, &resolved_segment_ids_to_delete);
1357
1358                    if remaining_points.len() >= 2 {
1359                        self.edit_vertical_points_constraint(&mut new_ast, constraint_id, remaining_points)
1360                            .map_err(KclErrorWithOutputs::no_outputs)?;
1361                    } else {
1362                        constraint_ids_set.insert(constraint_id);
1363                    }
1364                }
1365                Constraint::Fixed(fixed) => {
1366                    if fixed.points.iter().any(|fixed_point| {
1367                        self.segment_will_be_deleted(fixed_point.point, &resolved_segment_ids_to_delete)
1368                    }) {
1369                        constraint_ids_set.insert(constraint_id);
1370                    }
1371                }
1372                _ => {
1373                    // All other constraint types: if referenced by a segment -> delete the constraint
1374                    constraint_ids_set.insert(constraint_id);
1375                }
1376            }
1377        }
1378
1379        for constraint_id in constraint_ids_set {
1380            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1381                .map_err(KclErrorWithOutputs::no_outputs)?;
1382        }
1383        for segment_id in resolved_segment_ids_to_delete {
1384            self.delete_segment(&mut new_ast, sketch, segment_id)
1385                .map_err(KclErrorWithOutputs::no_outputs)?;
1386        }
1387
1388        self.execute_after_edit(
1389            ctx,
1390            sketch,
1391            sketch_block_ref,
1392            &mut new_ast,
1393            ExecuteAfterEditOptions {
1394                segment_ids_edited: Default::default(),
1395                edit_kind: EditDeleteKind::DeleteNonSketch,
1396                commit_solved_initial_guesses: true,
1397            },
1398        )
1399        .await
1400    }
1401
1402    async fn add_constraint(
1403        &mut self,
1404        ctx: &ExecutorContext,
1405        _version: Version,
1406        sketch: ObjectId,
1407        constraint: Constraint,
1408    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1409        // TODO: Check version.
1410
1411        // Save the original state as a backup - we'll restore it if anything fails
1412        let original_program = self.program.clone();
1413        let original_scene_graph = self.scene_graph.clone();
1414
1415        let mut new_ast = self.program.ast.clone();
1416        let sketch_block_ref = match constraint {
1417            Constraint::Coincident(coincident) => self
1418                .add_coincident(sketch, coincident, &mut new_ast)
1419                .await
1420                .map_err(KclErrorWithOutputs::no_outputs)?,
1421            Constraint::Distance(distance) => self
1422                .add_distance(sketch, distance, &mut new_ast)
1423                .await
1424                .map_err(KclErrorWithOutputs::no_outputs)?,
1425            Constraint::EqualRadius(equal_radius) => self
1426                .add_equal_radius(sketch, equal_radius, &mut new_ast)
1427                .await
1428                .map_err(KclErrorWithOutputs::no_outputs)?,
1429            Constraint::Fixed(fixed) => self
1430                .add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1431                .await
1432                .map_err(KclErrorWithOutputs::no_outputs)?,
1433            Constraint::HorizontalDistance(distance) => self
1434                .add_horizontal_distance(sketch, distance, &mut new_ast)
1435                .await
1436                .map_err(KclErrorWithOutputs::no_outputs)?,
1437            Constraint::VerticalDistance(distance) => self
1438                .add_vertical_distance(sketch, distance, &mut new_ast)
1439                .await
1440                .map_err(KclErrorWithOutputs::no_outputs)?,
1441            Constraint::Horizontal(horizontal) => self
1442                .add_horizontal(sketch, horizontal, &mut new_ast)
1443                .await
1444                .map_err(KclErrorWithOutputs::no_outputs)?,
1445            Constraint::LinesEqualLength(lines_equal_length) => self
1446                .add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1447                .await
1448                .map_err(KclErrorWithOutputs::no_outputs)?,
1449            Constraint::Midpoint(midpoint) => self
1450                .add_midpoint(sketch, midpoint, &mut new_ast)
1451                .await
1452                .map_err(KclErrorWithOutputs::no_outputs)?,
1453            Constraint::Parallel(parallel) => self
1454                .add_parallel(sketch, parallel, &mut new_ast)
1455                .await
1456                .map_err(KclErrorWithOutputs::no_outputs)?,
1457            Constraint::Perpendicular(perpendicular) => self
1458                .add_perpendicular(sketch, perpendicular, &mut new_ast)
1459                .await
1460                .map_err(KclErrorWithOutputs::no_outputs)?,
1461            Constraint::Radius(radius) => self
1462                .add_radius(sketch, radius, &mut new_ast)
1463                .await
1464                .map_err(KclErrorWithOutputs::no_outputs)?,
1465            Constraint::Diameter(diameter) => self
1466                .add_diameter(sketch, diameter, &mut new_ast)
1467                .await
1468                .map_err(KclErrorWithOutputs::no_outputs)?,
1469            Constraint::Symmetric(symmetric) => self
1470                .add_symmetric(sketch, symmetric, &mut new_ast)
1471                .await
1472                .map_err(KclErrorWithOutputs::no_outputs)?,
1473            Constraint::Vertical(vertical) => self
1474                .add_vertical(sketch, vertical, &mut new_ast)
1475                .await
1476                .map_err(KclErrorWithOutputs::no_outputs)?,
1477            Constraint::Angle(lines_at_angle) => self
1478                .add_angle(sketch, lines_at_angle, &mut new_ast)
1479                .await
1480                .map_err(KclErrorWithOutputs::no_outputs)?,
1481            Constraint::Tangent(tangent) => self
1482                .add_tangent(sketch, tangent, &mut new_ast)
1483                .await
1484                .map_err(KclErrorWithOutputs::no_outputs)?,
1485        };
1486
1487        let result = self
1488            .execute_after_add_constraint(ctx, sketch, sketch_block_ref, &mut new_ast)
1489            .await;
1490
1491        // If execution failed, restore the original state to prevent corruption
1492        if result.is_err() {
1493            self.program = original_program;
1494            self.scene_graph = original_scene_graph;
1495        }
1496
1497        result
1498    }
1499
1500    async fn chain_segment(
1501        &mut self,
1502        ctx: &ExecutorContext,
1503        version: Version,
1504        sketch: ObjectId,
1505        previous_segment_end_point_id: ObjectId,
1506        segment: SegmentCtor,
1507        _label: Option<String>,
1508    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1509        // TODO: Check version.
1510
1511        // First, add the segment (line) to get its start point ID
1512        let SegmentCtor::Line(line_ctor) = segment else {
1513            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1514                "chain_segment currently only supports Line segments, got {}",
1515                segment.human_friendly_kind_with_article(),
1516            ))));
1517        };
1518
1519        // Add the line segment first - this updates self.program and self.scene_graph
1520        let (_first_src_delta, first_scene_delta) = self.add_line(ctx, sketch, line_ctor).await?;
1521
1522        // Find the new line's start point ID from the updated scene graph
1523        // add_line updates self.scene_graph, so we can use that
1524        let new_line_id = first_scene_delta
1525            .new_objects
1526            .iter()
1527            .find(|&obj_id| {
1528                let obj = self.scene_graph.objects.get(obj_id.0);
1529                if let Some(obj) = obj {
1530                    matches!(
1531                        &obj.kind,
1532                        ObjectKind::Segment {
1533                            segment: Segment::Line(_)
1534                        }
1535                    )
1536                } else {
1537                    false
1538                }
1539            })
1540            .ok_or_else(|| {
1541                KclErrorWithOutputs::no_outputs(KclError::refactor(
1542                    "Failed to find new line segment in scene graph".to_string(),
1543                ))
1544            })?;
1545
1546        let new_line_obj = self.scene_graph.objects.get(new_line_id.0).ok_or_else(|| {
1547            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1548                "New line object not found: {new_line_id:?}"
1549            )))
1550        })?;
1551
1552        let ObjectKind::Segment {
1553            segment: new_line_segment,
1554        } = &new_line_obj.kind
1555        else {
1556            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1557                "Object is not a segment: {new_line_obj:?}"
1558            ))));
1559        };
1560
1561        let Segment::Line(new_line) = new_line_segment else {
1562            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1563                "Segment is not a line: {new_line_segment:?}"
1564            ))));
1565        };
1566
1567        let new_line_start_point_id = new_line.start;
1568
1569        // Now add the coincident constraint between the previous end point and the new line's start point.
1570        let coincident = Coincident {
1571            segments: vec![previous_segment_end_point_id.into(), new_line_start_point_id.into()],
1572        };
1573
1574        let (final_src_delta, final_scene_delta) = self
1575            .add_constraint(ctx, version, sketch, Constraint::Coincident(coincident))
1576            .await?;
1577
1578        // Combine new objects from the line addition and the constraint addition.
1579        // Both add_line and add_constraint now populate new_objects correctly.
1580        let mut combined_new_objects = first_scene_delta.new_objects.clone();
1581        combined_new_objects.extend(final_scene_delta.new_objects);
1582
1583        let scene_graph_delta = SceneGraphDelta {
1584            new_graph: self.scene_graph_for_ui(),
1585            invalidates_ids: false,
1586            new_objects: combined_new_objects,
1587            exec_outcome: final_scene_delta.exec_outcome,
1588        };
1589
1590        Ok((final_src_delta, scene_graph_delta))
1591    }
1592
1593    // Edit only the value, e.g. `distance(...) == 5mm` to `distance(...) == 7mm`.
1594    async fn edit_constraint_value(
1595        &mut self,
1596        ctx: &ExecutorContext,
1597        _version: Version,
1598        sketch: ObjectId,
1599        constraint_id: ObjectId,
1600        value_expression: String,
1601    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1602        // TODO: Check version.
1603        let sketch_block_ref =
1604            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1605
1606        let object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
1607            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Object not found: {constraint_id:?}")))
1608        })?;
1609        if !matches!(&object.kind, ObjectKind::Constraint { .. }) {
1610            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1611                "Object is not a constraint: {constraint_id:?}"
1612            ))));
1613        }
1614
1615        let mut new_ast = self.program.ast.clone();
1616
1617        // Parse the expression string into an AST node.
1618        let (parsed, errors) = Program::parse(&value_expression).map_err(|e| {
1619            KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(
1620                "Invalid constraint value",
1621                &e,
1622            )))
1623        })?;
1624        if !errors.is_empty() {
1625            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1626                format_compilation_issues("Invalid constraint value", &errors),
1627            )));
1628        }
1629        let mut parsed = parsed.ok_or_else(|| {
1630            KclErrorWithOutputs::no_outputs(KclError::refactor("No AST produced from value expression".to_string()))
1631        })?;
1632        if parsed.ast.body.is_empty() {
1633            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1634                "Empty value expression".to_string(),
1635            )));
1636        }
1637        let first = parsed.ast.body.remove(0);
1638        let ast::BodyItem::ExpressionStatement(expr_stmt) = first else {
1639            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
1640                "Value expression must be a simple expression".to_string(),
1641            )));
1642        };
1643
1644        let new_value: ast::BinaryPart = expr_stmt
1645            .inner
1646            .expression
1647            .try_into()
1648            .map_err(|e: String| KclErrorWithOutputs::no_outputs(KclError::refactor(e)))?;
1649
1650        self.mutate_ast(
1651            &mut new_ast,
1652            constraint_id,
1653            AstMutateCommand::EditConstraintValue { value: new_value },
1654        )
1655        .map_err(KclErrorWithOutputs::no_outputs)?;
1656
1657        self.execute_after_edit(
1658            ctx,
1659            sketch,
1660            sketch_block_ref,
1661            &mut new_ast,
1662            ExecuteAfterEditOptions {
1663                segment_ids_edited: Default::default(),
1664                edit_kind: EditDeleteKind::Edit,
1665                commit_solved_initial_guesses: true,
1666            },
1667        )
1668        .await
1669    }
1670
1671    async fn edit_distance_constraint_label_position(
1672        &mut self,
1673        ctx: &ExecutorContext,
1674        _version: Version,
1675        sketch: ObjectId,
1676        constraint_id: ObjectId,
1677        label_position: Point2d<Number>,
1678        anchor_segment_ids: Vec<ObjectId>,
1679    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1680        // TODO: Check version.
1681        let sketch_block_ref =
1682            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1683
1684        let mut new_ast = self.program.ast.clone();
1685        self.mutate_constraint_label_position(&mut new_ast, constraint_id, label_position)
1686            .map_err(KclErrorWithOutputs::no_outputs)?;
1687        let commit_solved_initial_guesses = self.next_edit_commits_solver_solutions.take().unwrap_or(true);
1688
1689        self.execute_after_edit(
1690            ctx,
1691            sketch,
1692            sketch_block_ref,
1693            &mut new_ast,
1694            ExecuteAfterEditOptions {
1695                segment_ids_edited: anchor_segment_ids.into_iter().collect(),
1696                edit_kind: EditDeleteKind::Edit,
1697                commit_solved_initial_guesses,
1698            },
1699        )
1700        .await
1701    }
1702
1703    /// Splitting a segment means creating a new segment, editing the old one, and then
1704    /// migrating a bunch of the constraints from the original segment to the new one
1705    /// (i.e. deleting them and re-adding them on the other segment).
1706    ///
1707    /// To keep this efficient we require as few executions as possible: we create the
1708    /// new segment first (to get its id), then do all edits and new constraints, and
1709    /// do all deletes at the end (since deletes invalidate ids).
1710    async fn batch_split_segment_operations(
1711        &mut self,
1712        ctx: &ExecutorContext,
1713        _version: Version,
1714        sketch: ObjectId,
1715        edit_segments: Vec<ExistingSegmentCtor>,
1716        add_constraints: Vec<Constraint>,
1717        delete_constraint_ids: Vec<ObjectId>,
1718        _new_segment_info: sketch::NewSegmentInfo,
1719    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1720        // TODO: Check version.
1721        let sketch_block_ref =
1722            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1723
1724        let mut new_ast = self.program.ast.clone();
1725        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1726
1727        // Step 1: Edit segments
1728        for segment in edit_segments {
1729            segment_ids_edited.insert(segment.id);
1730            match segment.ctor {
1731                SegmentCtor::Point(ctor) => self
1732                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1733                    .map_err(KclErrorWithOutputs::no_outputs)?,
1734                SegmentCtor::Line(ctor) => self
1735                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1736                    .map_err(KclErrorWithOutputs::no_outputs)?,
1737                SegmentCtor::Arc(ctor) => self
1738                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1739                    .map_err(KclErrorWithOutputs::no_outputs)?,
1740                SegmentCtor::Circle(ctor) => self
1741                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1742                    .map_err(KclErrorWithOutputs::no_outputs)?,
1743                SegmentCtor::ControlPointSpline(ctor) => self
1744                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1745                    .map_err(KclErrorWithOutputs::no_outputs)?,
1746            }
1747        }
1748
1749        // Step 2: Add all constraints
1750        for constraint in add_constraints {
1751            match constraint {
1752                Constraint::Coincident(coincident) => {
1753                    self.add_coincident(sketch, coincident, &mut new_ast)
1754                        .await
1755                        .map_err(KclErrorWithOutputs::no_outputs)?;
1756                }
1757                Constraint::Distance(distance) => {
1758                    self.add_distance(sketch, distance, &mut new_ast)
1759                        .await
1760                        .map_err(KclErrorWithOutputs::no_outputs)?;
1761                }
1762                Constraint::EqualRadius(equal_radius) => {
1763                    self.add_equal_radius(sketch, equal_radius, &mut new_ast)
1764                        .await
1765                        .map_err(KclErrorWithOutputs::no_outputs)?;
1766                }
1767                Constraint::Fixed(fixed) => {
1768                    self.add_fixed_constraints(sketch, fixed.points, &mut new_ast)
1769                        .await
1770                        .map_err(KclErrorWithOutputs::no_outputs)?;
1771                }
1772                Constraint::HorizontalDistance(distance) => {
1773                    self.add_horizontal_distance(sketch, distance, &mut new_ast)
1774                        .await
1775                        .map_err(KclErrorWithOutputs::no_outputs)?;
1776                }
1777                Constraint::VerticalDistance(distance) => {
1778                    self.add_vertical_distance(sketch, distance, &mut new_ast)
1779                        .await
1780                        .map_err(KclErrorWithOutputs::no_outputs)?;
1781                }
1782                Constraint::Horizontal(horizontal) => {
1783                    self.add_horizontal(sketch, horizontal, &mut new_ast)
1784                        .await
1785                        .map_err(KclErrorWithOutputs::no_outputs)?;
1786                }
1787                Constraint::LinesEqualLength(lines_equal_length) => {
1788                    self.add_lines_equal_length(sketch, lines_equal_length, &mut new_ast)
1789                        .await
1790                        .map_err(KclErrorWithOutputs::no_outputs)?;
1791                }
1792                Constraint::Midpoint(midpoint) => {
1793                    self.add_midpoint(sketch, midpoint, &mut new_ast)
1794                        .await
1795                        .map_err(KclErrorWithOutputs::no_outputs)?;
1796                }
1797                Constraint::Parallel(parallel) => {
1798                    self.add_parallel(sketch, parallel, &mut new_ast)
1799                        .await
1800                        .map_err(KclErrorWithOutputs::no_outputs)?;
1801                }
1802                Constraint::Perpendicular(perpendicular) => {
1803                    self.add_perpendicular(sketch, perpendicular, &mut new_ast)
1804                        .await
1805                        .map_err(KclErrorWithOutputs::no_outputs)?;
1806                }
1807                Constraint::Vertical(vertical) => {
1808                    self.add_vertical(sketch, vertical, &mut new_ast)
1809                        .await
1810                        .map_err(KclErrorWithOutputs::no_outputs)?;
1811                }
1812                Constraint::Diameter(diameter) => {
1813                    self.add_diameter(sketch, diameter, &mut new_ast)
1814                        .await
1815                        .map_err(KclErrorWithOutputs::no_outputs)?;
1816                }
1817                Constraint::Radius(radius) => {
1818                    self.add_radius(sketch, radius, &mut new_ast)
1819                        .await
1820                        .map_err(KclErrorWithOutputs::no_outputs)?;
1821                }
1822                Constraint::Symmetric(symmetric) => {
1823                    self.add_symmetric(sketch, symmetric, &mut new_ast)
1824                        .await
1825                        .map_err(KclErrorWithOutputs::no_outputs)?;
1826                }
1827                Constraint::Angle(angle) => {
1828                    self.add_angle(sketch, angle, &mut new_ast)
1829                        .await
1830                        .map_err(KclErrorWithOutputs::no_outputs)?;
1831                }
1832                Constraint::Tangent(tangent) => {
1833                    self.add_tangent(sketch, tangent, &mut new_ast)
1834                        .await
1835                        .map_err(KclErrorWithOutputs::no_outputs)?;
1836                }
1837            }
1838        }
1839
1840        // Step 3: Delete constraints (must be last since deletes can invalidate IDs)
1841        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1842
1843        let has_constraint_deletions = !constraint_ids_set.is_empty();
1844        for constraint_id in constraint_ids_set {
1845            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1846                .map_err(KclErrorWithOutputs::no_outputs)?;
1847        }
1848
1849        // Step 4: Execute once at the end
1850        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1851        // But we'll manually set invalidates_ids: true if we deleted constraints
1852        let (source_delta, mut scene_graph_delta) = self
1853            .execute_after_edit(
1854                ctx,
1855                sketch,
1856                sketch_block_ref,
1857                &mut new_ast,
1858                ExecuteAfterEditOptions {
1859                    segment_ids_edited,
1860                    edit_kind: EditDeleteKind::Edit,
1861                    commit_solved_initial_guesses: true,
1862                },
1863            )
1864            .await?;
1865
1866        // If we deleted constraints, set invalidates_ids: true
1867        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1868        if has_constraint_deletions {
1869            scene_graph_delta.invalidates_ids = true;
1870        }
1871
1872        Ok((source_delta, scene_graph_delta))
1873    }
1874
1875    async fn batch_tail_cut_operations(
1876        &mut self,
1877        ctx: &ExecutorContext,
1878        _version: Version,
1879        sketch: ObjectId,
1880        edit_segments: Vec<ExistingSegmentCtor>,
1881        add_constraints: Vec<Constraint>,
1882        delete_constraint_ids: Vec<ObjectId>,
1883        additional_edited_segment_ids: Vec<ObjectId>,
1884    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
1885        let sketch_block_ref =
1886            sketch_block_ref_from_id(&self.scene_graph, sketch).map_err(KclErrorWithOutputs::no_outputs)?;
1887
1888        let mut new_ast = self.program.ast.clone();
1889        let mut segment_ids_edited = AhashIndexSet::with_capacity_and_hasher(edit_segments.len(), Default::default());
1890
1891        // Step 1: Edit segments (usually a single segment for tail cut)
1892        for segment in edit_segments {
1893            segment_ids_edited.insert(segment.id);
1894            match segment.ctor {
1895                SegmentCtor::Point(ctor) => self
1896                    .edit_point(&mut new_ast, sketch, segment.id, ctor)
1897                    .map_err(KclErrorWithOutputs::no_outputs)?,
1898                SegmentCtor::Line(ctor) => self
1899                    .edit_line(&mut new_ast, sketch, segment.id, ctor)
1900                    .map_err(KclErrorWithOutputs::no_outputs)?,
1901                SegmentCtor::Arc(ctor) => self
1902                    .edit_arc(&mut new_ast, sketch, segment.id, ctor)
1903                    .map_err(KclErrorWithOutputs::no_outputs)?,
1904                SegmentCtor::Circle(ctor) => self
1905                    .edit_circle(&mut new_ast, sketch, segment.id, ctor)
1906                    .map_err(KclErrorWithOutputs::no_outputs)?,
1907                SegmentCtor::ControlPointSpline(ctor) => self
1908                    .edit_control_point_spline(&mut new_ast, sketch, segment.id, ctor)
1909                    .map_err(KclErrorWithOutputs::no_outputs)?,
1910            }
1911        }
1912
1913        segment_ids_edited.extend(additional_edited_segment_ids);
1914
1915        // Step 2: Add coincident constraints
1916        for constraint in add_constraints {
1917            match constraint {
1918                Constraint::Coincident(coincident) => {
1919                    self.add_coincident(sketch, coincident, &mut new_ast)
1920                        .await
1921                        .map_err(KclErrorWithOutputs::no_outputs)?;
1922                }
1923                other => {
1924                    return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
1925                        "unsupported constraint in tail cut batch: {other:?}"
1926                    ))));
1927                }
1928            }
1929        }
1930
1931        // Step 3: Delete constraints (if any)
1932        let constraint_ids_set = delete_constraint_ids.into_iter().collect::<AhashIndexSet<_>>();
1933
1934        let has_constraint_deletions = !constraint_ids_set.is_empty();
1935        for constraint_id in constraint_ids_set {
1936            self.delete_constraint(&mut new_ast, sketch, constraint_id)
1937                .map_err(KclErrorWithOutputs::no_outputs)?;
1938        }
1939
1940        // Step 4: Single execute_after_edit
1941        // Always use Edit (not DeleteNonSketch) because we're editing the sketch block, not deleting it
1942        // But we'll manually set invalidates_ids: true if we deleted constraints
1943        let (source_delta, mut scene_graph_delta) = self
1944            .execute_after_edit(
1945                ctx,
1946                sketch,
1947                sketch_block_ref,
1948                &mut new_ast,
1949                ExecuteAfterEditOptions {
1950                    segment_ids_edited,
1951                    edit_kind: EditDeleteKind::Edit,
1952                    commit_solved_initial_guesses: true,
1953                },
1954            )
1955            .await?;
1956
1957        // If we deleted constraints, set invalidates_ids: true
1958        // This is because constraint deletion invalidates IDs, even though we're not deleting the sketch block
1959        if has_constraint_deletions {
1960            scene_graph_delta.invalidates_ids = true;
1961        }
1962
1963        Ok((source_delta, scene_graph_delta))
1964    }
1965}
1966
1967impl FrontendState {
1968    /// Evaluate an input expression using the current model's settings and cached variables.
1969    pub async fn evaluate_expression(&self, ctx: &ExecutorContext, mut program: Program) -> ExecResult<ExecOutcome> {
1970        program.ast.inner_attrs.clone_from(&self.program.ast.inner_attrs);
1971        ctx.run_mock(&program, &MockConfig::default()).await
1972    }
1973
1974    pub async fn hack_set_program(&mut self, ctx: &ExecutorContext, program: Program) -> ExecResult<SetProgramOutcome> {
1975        self.program = program.clone();
1976
1977        // Execute so that the objects are updated and available for the next
1978        // API call.
1979        // This always uses engine execution (not mock) so that things are cached.
1980        // Engine execution now runs freedom analysis automatically.
1981        // Keep existing checkpoints alive here. History may still reference
1982        // older committed sketch states across a direct-edit boundary, and a
1983        // checkpoint restore is a full state replacement anyway. We append a
1984        // fresh baseline checkpoint after the full execution below.
1985        // Clear the freedom cache since IDs might have changed after direct editing
1986        // and we're about to run freedom analysis which will repopulate it.
1987        self.point_freedom_cache.clear();
1988        match ctx.run_with_caching(program).await {
1989            Ok(outcome) => {
1990                let outcome = self.update_state_after_exec(outcome, true);
1991                let checkpoint_id = self
1992                    .create_sketch_checkpoint(outcome.clone())
1993                    .await
1994                    .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.msg)))?;
1995                Ok(SetProgramOutcome::Success {
1996                    scene_graph: Box::new(self.scene_graph_for_ui()),
1997                    exec_outcome: Box::new(outcome),
1998                    checkpoint_id: Some(checkpoint_id),
1999                })
2000            }
2001            Err(mut err) => {
2002                // Don't return an error just because execution failed. Instead,
2003                // update state as much as possible.
2004                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2005                self.update_state_after_exec(outcome, true);
2006                err.scene_graph = Some(self.scene_graph_for_ui());
2007                Ok(SetProgramOutcome::ExecFailure { error: Box::new(err) })
2008            }
2009        }
2010    }
2011
2012    /// Decorate engine execution such that our state is updated and the scene
2013    /// graph is added to the return.
2014    pub async fn engine_execute(
2015        &mut self,
2016        ctx: &ExecutorContext,
2017        program: Program,
2018    ) -> Result<SceneGraphDelta, KclErrorWithOutputs> {
2019        self.program = program.clone();
2020
2021        // Engine execution now runs freedom analysis automatically. Clear the
2022        // freedom cache since IDs might have changed after direct editing, and
2023        // we're about to run freedom analysis which will repopulate it.
2024        self.point_freedom_cache.clear();
2025        match ctx.run_with_caching(program).await {
2026            Ok(outcome) => {
2027                let outcome = self.update_state_after_exec(outcome, true);
2028                Ok(SceneGraphDelta {
2029                    new_graph: self.scene_graph_for_ui(),
2030                    exec_outcome: outcome,
2031                    // We don't know what the new objects are.
2032                    new_objects: Default::default(),
2033                    // We don't know if IDs were invalidated.
2034                    invalidates_ids: Default::default(),
2035                })
2036            }
2037            Err(mut err) => {
2038                // Update state as much as possible, even when there's an error.
2039                let outcome = self.exec_outcome_from_exec_error(err.clone())?;
2040                self.update_state_after_exec(outcome, true);
2041                err.scene_graph = Some(self.scene_graph_for_ui());
2042                Err(err)
2043            }
2044        }
2045    }
2046
2047    fn exec_outcome_from_exec_error(&self, err: KclErrorWithOutputs) -> Result<ExecOutcome, KclErrorWithOutputs> {
2048        if matches!(err.error, KclError::EngineHangup { .. }) {
2049            // It's not ideal to special-case this, but this error is very
2050            // common during development, and it causes confusing downstream
2051            // errors that have nothing to do with the actual problem.
2052            return Err(err);
2053        }
2054
2055        let KclErrorWithOutputs {
2056            error,
2057            mut non_fatal,
2058            variables,
2059            operations,
2060            artifact_graph,
2061            scene_objects,
2062            source_range_to_object,
2063            var_solutions,
2064            refactor_metadata,
2065            filenames,
2066            source_files,
2067            default_planes,
2068            ..
2069        } = err;
2070
2071        non_fatal.push(CompilationIssue::fatal(issue_source_range(&error), error.get_message()));
2072
2073        Ok(ExecOutcome {
2074            variables,
2075            #[cfg(test)]
2076            test_program_memory: Default::default(),
2077            filenames,
2078            operations,
2079            artifact_graph,
2080            scene_objects,
2081            source_range_to_object,
2082            var_solutions,
2083            refactor_metadata,
2084            issues: non_fatal,
2085            source_files,
2086            default_planes,
2087        })
2088    }
2089
2090    async fn add_point(
2091        &mut self,
2092        ctx: &ExecutorContext,
2093        sketch: ObjectId,
2094        ctor: PointCtor,
2095    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2096        // Create updated KCL source from args.
2097        let at_ast = to_ast_point2d(&ctor.position)
2098            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2099        let point_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2100            callee: ast::Node::no_src(ast_sketch2_name(POINT_FN)),
2101            unlabeled: None,
2102            arguments: vec![ast::LabeledArg {
2103                label: Some(ast::Identifier::new(POINT_AT_PARAM)),
2104                arg: at_ast,
2105            }],
2106            digest: None,
2107            non_code_meta: Default::default(),
2108        })));
2109
2110        // Look up existing sketch.
2111        let sketch_id = sketch;
2112        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2113            #[cfg(target_arch = "wasm32")]
2114            web_sys::console::error_1(
2115                &format!(
2116                    "Sketch not found; sketch_id={sketch_id:?}, self.scene_graph.objects={:#?}",
2117                    self.scene_graph.objects
2118                )
2119                .into(),
2120            );
2121            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2122        })?;
2123        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2124            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2125                "Object is not a sketch, it is {}",
2126                sketch_object.kind.human_friendly_kind_with_article(),
2127            ))));
2128        };
2129        // Add the point to the AST of the sketch block.
2130        let mut new_ast = self.program.ast.clone();
2131        let (sketch_block_ref, _) = self
2132            .mutate_ast(
2133                &mut new_ast,
2134                sketch_id,
2135                AstMutateCommand::AddSketchBlockExprStmt { expr: point_ast },
2136            )
2137            .map_err(KclErrorWithOutputs::no_outputs)?;
2138        // Convert to string source to create real source ranges.
2139        let new_source = source_from_ast(&new_ast);
2140        // Parse the new KCL source.
2141        let new_program = parse_frontend_mutation_source(
2142            &new_source,
2143            "Error parsing KCL source after adding point",
2144            "No AST produced after adding point",
2145        )?;
2146
2147        let point_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2148            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2149                "Source range of point not found in sketch block: {sketch_block_ref:?}; {err:?}"
2150            )))
2151        })?;
2152
2153        // Make sure to only set this if there are no errors.
2154        self.program = new_program.clone();
2155
2156        // Truncate after the sketch block for mock execution.
2157        let mut truncated_program = new_program;
2158        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2159            .map_err(KclErrorWithOutputs::no_outputs)?;
2160
2161        // Execute.
2162        let outcome = ctx
2163            .run_mock(
2164                &truncated_program,
2165                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2166            )
2167            .await?;
2168
2169        let new_object_ids = {
2170            let make_err =
2171                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2172            let segment_id = outcome
2173                .source_range_to_object
2174                .get(&point_node_ref.range)
2175                .copied()
2176                .ok_or_else(|| make_err(format!("Source range of point not found: {point_node_ref:?}")))?;
2177            let segment_object = outcome
2178                .scene_objects
2179                .get(segment_id.0)
2180                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2181            let ObjectKind::Segment { segment } = &segment_object.kind else {
2182                return Err(make_err(format!(
2183                    "Object is not a segment, it is {}",
2184                    segment_object.kind.human_friendly_kind_with_article()
2185                )));
2186            };
2187            let Segment::Point(_) = segment else {
2188                return Err(make_err(format!(
2189                    "Segment is not a point, it is {}",
2190                    segment.human_friendly_kind_with_article()
2191                )));
2192            };
2193            vec![segment_id]
2194        };
2195        let src_delta = SourceDelta { text: new_source };
2196        // Uses .no_freedom_analysis() so freedom_analysis: false
2197        let outcome = self.update_state_after_exec(outcome, false);
2198        let scene_graph_delta = SceneGraphDelta {
2199            new_graph: self.scene_graph_for_ui(),
2200            invalidates_ids: false,
2201            new_objects: new_object_ids,
2202            exec_outcome: outcome,
2203        };
2204        Ok((src_delta, scene_graph_delta))
2205    }
2206
2207    async fn add_line(
2208        &mut self,
2209        ctx: &ExecutorContext,
2210        sketch: ObjectId,
2211        ctor: LineCtor,
2212    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2213        // Create updated KCL source from args.
2214        let start_ast = to_ast_point2d(&ctor.start)
2215            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2216        let end_ast = to_ast_point2d(&ctor.end)
2217            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2218        let mut arguments = vec![
2219            ast::LabeledArg {
2220                label: Some(ast::Identifier::new(LINE_START_PARAM)),
2221                arg: start_ast,
2222            },
2223            ast::LabeledArg {
2224                label: Some(ast::Identifier::new(LINE_END_PARAM)),
2225                arg: end_ast,
2226            },
2227        ];
2228        // Add construction kwarg if construction is Some(true)
2229        if ctor.construction == Some(true) {
2230            arguments.push(ast::LabeledArg {
2231                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2232                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2233                    value: ast::LiteralValue::Bool(true),
2234                    raw: "true".to_string(),
2235                    digest: None,
2236                }))),
2237            });
2238        }
2239        let line_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2240            callee: ast::Node::no_src(ast_sketch2_name(LINE_FN)),
2241            unlabeled: None,
2242            arguments,
2243            digest: None,
2244            non_code_meta: Default::default(),
2245        })));
2246
2247        // Look up existing sketch.
2248        let sketch_id = sketch;
2249        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2250            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2251        })?;
2252        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2253            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2254                "Object is not a sketch, it is {}",
2255                sketch_object.kind.human_friendly_kind_with_article(),
2256            ))));
2257        };
2258        // Add the line to the AST of the sketch block.
2259        let mut new_ast = self.program.ast.clone();
2260        let (sketch_block_ref, _) = self
2261            .mutate_ast(
2262                &mut new_ast,
2263                sketch_id,
2264                AstMutateCommand::AddSketchBlockExprStmt { expr: line_ast },
2265            )
2266            .map_err(KclErrorWithOutputs::no_outputs)?;
2267        // Convert to string source to create real source ranges.
2268        let new_source = source_from_ast(&new_ast);
2269        // Parse the new KCL source.
2270        let new_program = parse_frontend_mutation_source(
2271            &new_source,
2272            "Error parsing KCL source after adding line",
2273            "No AST produced after adding line",
2274        )?;
2275
2276        let line_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2277            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2278                "Source range of line not found in sketch block: {sketch_block_ref:?}; {err:?}"
2279            )))
2280        })?;
2281
2282        // Make sure to only set this if there are no errors.
2283        self.program = new_program.clone();
2284
2285        // Truncate after the sketch block for mock execution.
2286        let mut truncated_program = new_program;
2287        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2288            .map_err(KclErrorWithOutputs::no_outputs)?;
2289
2290        // Execute.
2291        let outcome = ctx
2292            .run_mock(
2293                &truncated_program,
2294                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2295            )
2296            .await?;
2297
2298        let new_object_ids = {
2299            let make_err =
2300                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2301            let segment_id = outcome
2302                .source_range_to_object
2303                .get(&line_node_ref.range)
2304                .copied()
2305                .ok_or_else(|| make_err(format!("Source range of line not found: {line_node_ref:?}")))?;
2306            let segment_object = outcome
2307                .scene_object_by_id(segment_id)
2308                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2309            let ObjectKind::Segment { segment } = &segment_object.kind else {
2310                return Err(make_err(format!(
2311                    "Object is not a segment, it is {}",
2312                    segment_object.kind.human_friendly_kind_with_article()
2313                )));
2314            };
2315            let Segment::Line(line) = segment else {
2316                return Err(make_err(format!(
2317                    "Segment is not a line, it is {}",
2318                    segment.human_friendly_kind_with_article()
2319                )));
2320            };
2321            vec![line.start, line.end, segment_id]
2322        };
2323        let src_delta = SourceDelta { text: new_source };
2324        // Uses .no_freedom_analysis() so freedom_analysis: false
2325        let outcome = self.update_state_after_exec(outcome, false);
2326        let scene_graph_delta = SceneGraphDelta {
2327            new_graph: self.scene_graph_for_ui(),
2328            invalidates_ids: false,
2329            new_objects: new_object_ids,
2330            exec_outcome: outcome,
2331        };
2332        Ok((src_delta, scene_graph_delta))
2333    }
2334
2335    async fn add_arc(
2336        &mut self,
2337        ctx: &ExecutorContext,
2338        sketch: ObjectId,
2339        ctor: ArcCtor,
2340    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2341        // Create updated KCL source from args.
2342        let start_ast = to_ast_point2d(&ctor.start)
2343            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2344        let end_ast = to_ast_point2d(&ctor.end)
2345            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2346        let center_ast = to_ast_point2d(&ctor.center)
2347            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2348        let mut arguments = vec![
2349            ast::LabeledArg {
2350                label: Some(ast::Identifier::new(ARC_START_PARAM)),
2351                arg: start_ast,
2352            },
2353            ast::LabeledArg {
2354                label: Some(ast::Identifier::new(ARC_END_PARAM)),
2355                arg: end_ast,
2356            },
2357            ast::LabeledArg {
2358                label: Some(ast::Identifier::new(ARC_CENTER_PARAM)),
2359                arg: center_ast,
2360            },
2361        ];
2362        // Add direction kwarg if it's clockwise, since counterclockwise is the
2363        // default.
2364        if ctor.direction == Some(ArcDirection::Cw) {
2365            arguments.push(ast::LabeledArg {
2366                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
2367                arg: ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME))),
2368            });
2369        }
2370        // Add construction kwarg if construction is Some(true)
2371        if ctor.construction == Some(true) {
2372            arguments.push(ast::LabeledArg {
2373                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2374                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2375                    value: ast::LiteralValue::Bool(true),
2376                    raw: "true".to_string(),
2377                    digest: None,
2378                }))),
2379            });
2380        }
2381        let arc_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2382            callee: ast::Node::no_src(ast_sketch2_name(ARC_FN)),
2383            unlabeled: None,
2384            arguments,
2385            digest: None,
2386            non_code_meta: Default::default(),
2387        })));
2388
2389        // Look up existing sketch.
2390        let sketch_id = sketch;
2391        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2392            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2393        })?;
2394        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2395            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2396                "Object is not a sketch, it is {}",
2397                sketch_object.kind.human_friendly_kind_with_article(),
2398            ))));
2399        };
2400        // Add the arc to the AST of the sketch block.
2401        let mut new_ast = self.program.ast.clone();
2402        let (sketch_block_ref, _) = self
2403            .mutate_ast(
2404                &mut new_ast,
2405                sketch_id,
2406                AstMutateCommand::AddSketchBlockExprStmt { expr: arc_ast },
2407            )
2408            .map_err(KclErrorWithOutputs::no_outputs)?;
2409        // Convert to string source to create real source ranges.
2410        let new_source = source_from_ast(&new_ast);
2411        // Parse the new KCL source.
2412        let new_program = parse_frontend_mutation_source(
2413            &new_source,
2414            "Error parsing KCL source after adding arc",
2415            "No AST produced after adding arc",
2416        )?;
2417
2418        let arc_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2419            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2420                "Source range of arc not found in sketch block: {sketch_block_ref:?}; {err:?}"
2421            )))
2422        })?;
2423
2424        // Make sure to only set this if there are no errors.
2425        self.program = new_program.clone();
2426
2427        // Truncate after the sketch block for mock execution.
2428        let mut truncated_program = new_program;
2429        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2430            .map_err(KclErrorWithOutputs::no_outputs)?;
2431
2432        // Execute.
2433        let outcome = ctx
2434            .run_mock(
2435                &truncated_program,
2436                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2437            )
2438            .await?;
2439
2440        let new_object_ids = {
2441            let make_err =
2442                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2443            let segment_id = outcome
2444                .source_range_to_object
2445                .get(&arc_node_ref.range)
2446                .copied()
2447                .ok_or_else(|| make_err(format!("Source range of arc not found: {arc_node_ref:?}")))?;
2448            let segment_object = outcome
2449                .scene_objects
2450                .get(segment_id.0)
2451                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2452            let ObjectKind::Segment { segment } = &segment_object.kind else {
2453                return Err(make_err(format!(
2454                    "Object is not a segment, it is {}",
2455                    segment_object.kind.human_friendly_kind_with_article()
2456                )));
2457            };
2458            let Segment::Arc(arc) = segment else {
2459                return Err(make_err(format!(
2460                    "Segment is not an arc, it is {}",
2461                    segment.human_friendly_kind_with_article()
2462                )));
2463            };
2464            vec![arc.start, arc.end, arc.center, segment_id]
2465        };
2466        let src_delta = SourceDelta { text: new_source };
2467        // Uses .no_freedom_analysis() so freedom_analysis: false
2468        let outcome = self.update_state_after_exec(outcome, false);
2469        let scene_graph_delta = SceneGraphDelta {
2470            new_graph: self.scene_graph_for_ui(),
2471            invalidates_ids: false,
2472            new_objects: new_object_ids,
2473            exec_outcome: outcome,
2474        };
2475        Ok((src_delta, scene_graph_delta))
2476    }
2477
2478    async fn add_circle(
2479        &mut self,
2480        ctx: &ExecutorContext,
2481        sketch: ObjectId,
2482        ctor: CircleCtor,
2483    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2484        // Create updated KCL source from args.
2485        let start_ast = to_ast_point2d(&ctor.start)
2486            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2487        let center_ast = to_ast_point2d(&ctor.center)
2488            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2489        let mut arguments = vec![
2490            ast::LabeledArg {
2491                label: Some(ast::Identifier::new(CIRCLE_START_PARAM)),
2492                arg: start_ast,
2493            },
2494            ast::LabeledArg {
2495                label: Some(ast::Identifier::new(CIRCLE_CENTER_PARAM)),
2496                arg: center_ast,
2497            },
2498        ];
2499        // Add construction kwarg if construction is Some(true)
2500        if ctor.construction == Some(true) {
2501            arguments.push(ast::LabeledArg {
2502                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2503                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2504                    value: ast::LiteralValue::Bool(true),
2505                    raw: "true".to_string(),
2506                    digest: None,
2507                }))),
2508            });
2509        }
2510        let circle_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2511            callee: ast::Node::no_src(ast_sketch2_name(CIRCLE_FN)),
2512            unlabeled: None,
2513            arguments,
2514            digest: None,
2515            non_code_meta: Default::default(),
2516        })));
2517
2518        // Look up existing sketch.
2519        let sketch_id = sketch;
2520        let sketch_object = self.scene_graph.objects.get(sketch_id.0).ok_or_else(|| {
2521            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2522        })?;
2523        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2524            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2525                "Object is not a sketch, it is {}",
2526                sketch_object.kind.human_friendly_kind_with_article(),
2527            ))));
2528        };
2529        // Add the circle to the AST of the sketch block.
2530        let mut new_ast = self.program.ast.clone();
2531        let (sketch_block_ref, _) = self
2532            .mutate_ast(
2533                &mut new_ast,
2534                sketch_id,
2535                AstMutateCommand::AddSketchBlockVarDecl {
2536                    prefix: CIRCLE_VARIABLE.to_owned(),
2537                    expr: circle_ast,
2538                },
2539            )
2540            .map_err(KclErrorWithOutputs::no_outputs)?;
2541        // Convert to string source to create real source ranges.
2542        let new_source = source_from_ast(&new_ast);
2543        // Parse the new KCL source.
2544        let new_program = parse_frontend_mutation_source(
2545            &new_source,
2546            "Error parsing KCL source after adding circle",
2547            "No AST produced after adding circle",
2548        )?;
2549
2550        let circle_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2551            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2552                "Source range of circle not found in sketch block: {sketch_block_ref:?}; {err:?}"
2553            )))
2554        })?;
2555
2556        // Make sure to only set this if there are no errors.
2557        self.program = new_program.clone();
2558
2559        // Truncate after the sketch block for mock execution.
2560        let mut truncated_program = new_program;
2561        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2562            .map_err(KclErrorWithOutputs::no_outputs)?;
2563
2564        // Execute.
2565        let outcome = ctx
2566            .run_mock(
2567                &truncated_program,
2568                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2569            )
2570            .await?;
2571
2572        let new_object_ids = {
2573            let make_err =
2574                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2575            let segment_id = outcome
2576                .source_range_to_object
2577                .get(&circle_node_ref.range)
2578                .copied()
2579                .ok_or_else(|| make_err(format!("Source range of circle not found: {circle_node_ref:?}")))?;
2580            let segment_object = outcome
2581                .scene_objects
2582                .get(segment_id.0)
2583                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2584            let ObjectKind::Segment { segment } = &segment_object.kind else {
2585                return Err(make_err(format!(
2586                    "Object is not a segment, it is {}",
2587                    segment_object.kind.human_friendly_kind_with_article()
2588                )));
2589            };
2590            let Segment::Circle(circle) = segment else {
2591                return Err(make_err(format!(
2592                    "Segment is not a circle, it is {}",
2593                    segment.human_friendly_kind_with_article()
2594                )));
2595            };
2596            vec![circle.start, circle.center, segment_id]
2597        };
2598        let src_delta = SourceDelta { text: new_source };
2599        // Uses .no_freedom_analysis() so freedom_analysis: false
2600        let outcome = self.update_state_after_exec(outcome, false);
2601        let scene_graph_delta = SceneGraphDelta {
2602            new_graph: self.scene_graph_for_ui(),
2603            invalidates_ids: false,
2604            new_objects: new_object_ids,
2605            exec_outcome: outcome,
2606        };
2607        Ok((src_delta, scene_graph_delta))
2608    }
2609
2610    async fn add_control_point_spline(
2611        &mut self,
2612        ctx: &ExecutorContext,
2613        sketch: ObjectId,
2614        ctor: ControlPointSplineCtor,
2615    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
2616        let new_program = ensure_control_point_spline_experimental_features(&self.program)
2617            .map_err(KclErrorWithOutputs::no_outputs)?;
2618
2619        let points_ast = to_ast_point2d_array(&ctor.points)
2620            .map_err(|err| KclErrorWithOutputs::no_outputs(KclError::refactor(err.to_string())))?;
2621        let mut arguments = vec![ast::LabeledArg {
2622            label: Some(ast::Identifier::new(CONTROL_POINT_SPLINE_POINTS_PARAM)),
2623            arg: points_ast,
2624        }];
2625        if ctor.construction == Some(true) {
2626            arguments.push(ast::LabeledArg {
2627                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
2628                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
2629                    value: ast::LiteralValue::Bool(true),
2630                    raw: "true".to_string(),
2631                    digest: None,
2632                }))),
2633            });
2634        }
2635        let spline_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
2636            callee: ast::Node::no_src(ast_sketch2_name(CONTROL_POINT_SPLINE_FN)),
2637            unlabeled: None,
2638            arguments,
2639            digest: None,
2640            non_code_meta: Default::default(),
2641        })));
2642
2643        let sketch_object = self.scene_graph.objects.get(sketch.0).ok_or_else(|| {
2644            KclErrorWithOutputs::no_outputs(KclError::refactor(format!("Sketch not found: {sketch:?}")))
2645        })?;
2646        let ObjectKind::Sketch(_) = &sketch_object.kind else {
2647            return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2648                "Object is not a sketch, it is {}",
2649                sketch_object.kind.human_friendly_kind_with_article(),
2650            ))));
2651        };
2652
2653        let mut new_ast = new_program.ast.clone();
2654        let (sketch_block_ref, _) = self
2655            .mutate_ast(
2656                &mut new_ast,
2657                sketch,
2658                AstMutateCommand::AddSketchBlockExprStmt { expr: spline_ast },
2659            )
2660            .map_err(KclErrorWithOutputs::no_outputs)?;
2661        let new_source = source_from_ast(&new_ast);
2662        let new_program = parse_frontend_mutation_source(
2663            &new_source,
2664            "Error parsing KCL source after adding controlPointSpline",
2665            "No AST produced after adding controlPointSpline",
2666        )?;
2667
2668        let spline_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
2669            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
2670                "Source range of controlPointSpline not found in sketch block: {sketch_block_ref:?}; {err:?}"
2671            )))
2672        })?;
2673
2674        self.program = new_program.clone();
2675
2676        let mut truncated_program = new_program;
2677        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
2678            .map_err(KclErrorWithOutputs::no_outputs)?;
2679
2680        let outcome = ctx
2681            .run_mock(
2682                &truncated_program,
2683                &MockConfig::new_sketch_mode(sketch).no_freedom_analysis(),
2684            )
2685            .await?;
2686
2687        let new_object_ids = {
2688            let make_err =
2689                |msg: String| KclErrorWithOutputs::from_error_outcome(KclError::refactor(msg), outcome.clone());
2690            let segment_id = outcome
2691                .source_range_to_object
2692                .get(&spline_node_ref.range)
2693                .copied()
2694                .ok_or_else(|| {
2695                    make_err(format!(
2696                        "Source range of controlPointSpline not found: {spline_node_ref:?}"
2697                    ))
2698                })?;
2699            let segment_object = outcome
2700                .scene_objects
2701                .get(segment_id.0)
2702                .ok_or_else(|| make_err(format!("Segment not found: {segment_id:?}")))?;
2703            let ObjectKind::Segment { segment } = &segment_object.kind else {
2704                return Err(make_err(format!(
2705                    "Object is not a segment, it is {}",
2706                    segment_object.kind.human_friendly_kind_with_article()
2707                )));
2708            };
2709            let Segment::ControlPointSpline(spline) = segment else {
2710                return Err(make_err(format!(
2711                    "Segment is not a control point spline, it is {}",
2712                    segment.human_friendly_kind_with_article()
2713                )));
2714            };
2715
2716            let mut ids = outcome
2717                .scene_objects
2718                .iter()
2719                .filter_map(|obj| match &obj.kind {
2720                    ObjectKind::Segment {
2721                        segment: Segment::Line(line),
2722                    } if line.owner == Some(segment_id) => Some(obj.id),
2723                    _ => None,
2724                })
2725                .collect::<Vec<_>>();
2726            ids.extend(spline.controls.clone());
2727            ids.push(segment_id);
2728            ids
2729        };
2730        let src_delta = SourceDelta { text: new_source };
2731        let outcome = self.update_state_after_exec(outcome, false);
2732        let scene_graph_delta = SceneGraphDelta {
2733            new_graph: self.scene_graph_for_ui(),
2734            invalidates_ids: false,
2735            new_objects: new_object_ids,
2736            exec_outcome: outcome,
2737        };
2738        Ok((src_delta, scene_graph_delta))
2739    }
2740
2741    fn edit_point(
2742        &mut self,
2743        new_ast: &mut ast::Node<ast::Program>,
2744        sketch: ObjectId,
2745        point: ObjectId,
2746        ctor: PointCtor,
2747    ) -> Result<(), KclError> {
2748        // Create updated KCL source from args.
2749        let new_at_ast = to_ast_point2d(&ctor.position).map_err(|err| KclError::refactor(err.to_string()))?;
2750
2751        // Look up existing sketch.
2752        let sketch_id = sketch;
2753        let sketch_object = self
2754            .scene_graph
2755            .objects
2756            .get(sketch_id.0)
2757            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2758        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2759            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2760        };
2761        sketch.segments.iter().find(|o| **o == point).ok_or_else(|| {
2762            KclError::refactor(format!("Point not found in sketch: point={point:?}, sketch={sketch:?}"))
2763        })?;
2764        // Look up existing point.
2765        let point_id = point;
2766        let point_object = self
2767            .scene_graph
2768            .objects
2769            .get(point_id.0)
2770            .ok_or_else(|| KclError::refactor(format!("Point not found in scene graph: point={point:?}")))?;
2771        let ObjectKind::Segment {
2772            segment: Segment::Point(point),
2773        } = &point_object.kind
2774        else {
2775            return Err(KclError::refactor(format!(
2776                "Object is not a point segment: {point_object:?}"
2777            )));
2778        };
2779
2780        // If the point is part of a line or arc, edit the line/arc instead.
2781        if let Some(owner_id) = point.owner {
2782            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
2783                KclError::refactor(format!(
2784                    "Internal: Owner of point not found in scene graph: owner={owner_id:?}",
2785                ))
2786            })?;
2787            let ObjectKind::Segment { segment } = &owner_object.kind else {
2788                return Err(KclError::refactor(format!(
2789                    "Internal: Owner of point is not a segment, but found {}",
2790                    owner_object.kind.human_friendly_kind_with_article()
2791                )));
2792            };
2793
2794            // Handle Line owner
2795            if let Segment::Line(line) = segment {
2796                let SegmentCtor::Line(line_ctor) = &line.ctor else {
2797                    return Err(KclError::refactor(format!(
2798                        "Internal: Owner of point does not have line ctor, but found {}",
2799                        line.ctor.human_friendly_kind_with_article()
2800                    )));
2801                };
2802                let mut line_ctor = line_ctor.clone();
2803                // Which end of the line is this point?
2804                if line.start == point_id {
2805                    line_ctor.start = ctor.position;
2806                } else if line.end == point_id {
2807                    line_ctor.end = ctor.position;
2808                } else {
2809                    return Err(KclError::refactor(format!(
2810                        "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
2811                    )));
2812                }
2813                return self.edit_line(new_ast, sketch_id, owner_id, line_ctor);
2814            }
2815
2816            // Handle Arc owner
2817            if let Segment::Arc(arc) = segment {
2818                let SegmentCtor::Arc(arc_ctor) = &arc.ctor else {
2819                    return Err(KclError::refactor(format!(
2820                        "Internal: Owner of point does not have arc ctor, but found {}",
2821                        arc.ctor.human_friendly_kind_with_article()
2822                    )));
2823                };
2824                let mut arc_ctor = arc_ctor.clone();
2825                // Which point of the arc is this? (center, start, or end)
2826                if arc.center == point_id {
2827                    arc_ctor.center = ctor.position;
2828                } else if arc.start == point_id {
2829                    arc_ctor.start = ctor.position;
2830                } else if arc.end == point_id {
2831                    arc_ctor.end = ctor.position;
2832                } else {
2833                    return Err(KclError::refactor(format!(
2834                        "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
2835                    )));
2836                }
2837                return self.edit_arc(new_ast, sketch_id, owner_id, arc_ctor);
2838            }
2839
2840            // Handle Circle owner
2841            if let Segment::Circle(circle) = segment {
2842                let SegmentCtor::Circle(circle_ctor) = &circle.ctor else {
2843                    return Err(KclError::refactor(format!(
2844                        "Internal: Owner of point does not have circle ctor, but found {}",
2845                        circle.ctor.human_friendly_kind_with_article()
2846                    )));
2847                };
2848                let mut circle_ctor = circle_ctor.clone();
2849                if circle.center == point_id {
2850                    circle_ctor.center = ctor.position;
2851                } else if circle.start == point_id {
2852                    circle_ctor.start = ctor.position;
2853                } else {
2854                    return Err(KclError::refactor(format!(
2855                        "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
2856                    )));
2857                }
2858                return self.edit_circle(new_ast, sketch_id, owner_id, circle_ctor);
2859            }
2860
2861            if let Segment::ControlPointSpline(spline) = segment {
2862                let SegmentCtor::ControlPointSpline(spline_ctor) = &spline.ctor else {
2863                    return Err(KclError::refactor(format!(
2864                        "Internal: Owner of point does not have controlPointSpline ctor, but found {}",
2865                        spline.ctor.human_friendly_kind_with_article()
2866                    )));
2867                };
2868                let mut spline_ctor = spline_ctor.clone();
2869                let Some(control_index) = spline.controls.iter().position(|id| *id == point_id) else {
2870                    return Err(KclError::refactor(format!(
2871                        "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
2872                    )));
2873                };
2874                spline_ctor.points[control_index] = ctor.position;
2875                return self.edit_control_point_spline(new_ast, sketch_id, owner_id, spline_ctor);
2876            }
2877
2878            // If owner is neither Line, Arc, nor Circle, allow editing the point directly
2879            // (fall through to the point editing logic below)
2880        }
2881
2882        // Modify the point AST.
2883        self.mutate_ast(new_ast, point_id, AstMutateCommand::EditPoint { at: new_at_ast })?;
2884        Ok(())
2885    }
2886
2887    fn edit_line(
2888        &mut self,
2889        new_ast: &mut ast::Node<ast::Program>,
2890        sketch: ObjectId,
2891        line: ObjectId,
2892        ctor: LineCtor,
2893    ) -> Result<(), KclError> {
2894        // Create updated KCL source from args.
2895        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2896        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2897
2898        // Look up existing sketch.
2899        let sketch_id = sketch;
2900        let sketch_object = self
2901            .scene_graph
2902            .objects
2903            .get(sketch_id.0)
2904            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2905        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2906            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2907        };
2908        sketch
2909            .segments
2910            .iter()
2911            .find(|o| **o == line)
2912            .ok_or_else(|| KclError::refactor(format!("Line not found in sketch: line={line:?}, sketch={sketch:?}")))?;
2913        // Look up existing line.
2914        let line_id = line;
2915        let line_object = self
2916            .scene_graph
2917            .objects
2918            .get(line_id.0)
2919            .ok_or_else(|| KclError::refactor(format!("Line not found in scene graph: line={line:?}")))?;
2920        let ObjectKind::Segment { .. } = &line_object.kind else {
2921            let kind = line_object.kind.human_friendly_kind_with_article();
2922            return Err(KclError::refactor(format!(
2923                "This constraint only works on Segments, but you selected {kind}"
2924            )));
2925        };
2926
2927        // Modify the line AST.
2928        self.mutate_ast(
2929            new_ast,
2930            line_id,
2931            AstMutateCommand::EditLine {
2932                start: new_start_ast,
2933                end: new_end_ast,
2934                construction: ctor.construction,
2935            },
2936        )?;
2937        Ok(())
2938    }
2939
2940    fn edit_arc(
2941        &mut self,
2942        new_ast: &mut ast::Node<ast::Program>,
2943        sketch: ObjectId,
2944        arc: ObjectId,
2945        ctor: ArcCtor,
2946    ) -> Result<(), KclError> {
2947        // Create updated KCL source from args.
2948        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
2949        let new_end_ast = to_ast_point2d(&ctor.end).map_err(|err| KclError::refactor(err.to_string()))?;
2950        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
2951
2952        // Look up existing sketch.
2953        let sketch_id = sketch;
2954        let sketch_object = self
2955            .scene_graph
2956            .objects
2957            .get(sketch_id.0)
2958            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
2959        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
2960            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
2961        };
2962        sketch
2963            .segments
2964            .iter()
2965            .find(|o| **o == arc)
2966            .ok_or_else(|| KclError::refactor(format!("Arc not found in sketch: arc={arc:?}, sketch={sketch:?}")))?;
2967        // Look up existing arc.
2968        let arc_id = arc;
2969        let arc_object = self
2970            .scene_graph
2971            .objects
2972            .get(arc_id.0)
2973            .ok_or_else(|| KclError::refactor(format!("Arc not found in scene graph: arc={arc:?}")))?;
2974        let ObjectKind::Segment { .. } = &arc_object.kind else {
2975            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
2976        };
2977
2978        // Modify the arc AST.
2979        self.mutate_ast(
2980            new_ast,
2981            arc_id,
2982            AstMutateCommand::EditArc {
2983                start: new_start_ast,
2984                end: new_end_ast,
2985                center: new_center_ast,
2986                direction: ctor.direction,
2987                construction: ctor.construction,
2988            },
2989        )?;
2990        Ok(())
2991    }
2992
2993    fn edit_circle(
2994        &mut self,
2995        new_ast: &mut ast::Node<ast::Program>,
2996        sketch: ObjectId,
2997        circle: ObjectId,
2998        ctor: CircleCtor,
2999    ) -> Result<(), KclError> {
3000        // Create updated KCL source from args.
3001        let new_start_ast = to_ast_point2d(&ctor.start).map_err(|err| KclError::refactor(err.to_string()))?;
3002        let new_center_ast = to_ast_point2d(&ctor.center).map_err(|err| KclError::refactor(err.to_string()))?;
3003
3004        // Look up existing sketch.
3005        let sketch_id = sketch;
3006        let sketch_object = self
3007            .scene_graph
3008            .objects
3009            .get(sketch_id.0)
3010            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3011        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3012            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3013        };
3014        sketch.segments.iter().find(|o| **o == circle).ok_or_else(|| {
3015            KclError::refactor(format!(
3016                "Circle not found in sketch: circle={circle:?}, sketch={sketch:?}"
3017            ))
3018        })?;
3019        // Look up existing circle.
3020        let circle_id = circle;
3021        let circle_object = self
3022            .scene_graph
3023            .objects
3024            .get(circle_id.0)
3025            .ok_or_else(|| KclError::refactor(format!("Circle not found in scene graph: circle={circle:?}")))?;
3026        let ObjectKind::Segment { .. } = &circle_object.kind else {
3027            return Err(KclError::refactor(format!(
3028                "Object is not a segment: {circle_object:?}"
3029            )));
3030        };
3031
3032        // Modify the circle AST.
3033        self.mutate_ast(
3034            new_ast,
3035            circle_id,
3036            AstMutateCommand::EditCircle {
3037                start: new_start_ast,
3038                center: new_center_ast,
3039                construction: ctor.construction,
3040            },
3041        )?;
3042        Ok(())
3043    }
3044
3045    fn edit_control_point_spline(
3046        &mut self,
3047        new_ast: &mut ast::Node<ast::Program>,
3048        sketch: ObjectId,
3049        spline: ObjectId,
3050        ctor: ControlPointSplineCtor,
3051    ) -> Result<(), KclError> {
3052        let points_ast = to_ast_point2d_array(&ctor.points).map_err(|err| KclError::refactor(err.to_string()))?;
3053
3054        let sketch_object = self
3055            .scene_graph
3056            .objects
3057            .get(sketch.0)
3058            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3059        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3060            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3061        };
3062        sketch.segments.iter().find(|o| **o == spline).ok_or_else(|| {
3063            KclError::refactor(format!(
3064                "Control point spline not found in sketch: spline={spline:?}, sketch={sketch:?}"
3065            ))
3066        })?;
3067
3068        let spline_object =
3069            self.scene_graph.objects.get(spline.0).ok_or_else(|| {
3070                KclError::refactor(format!("Control point spline not found in scene graph: {spline:?}"))
3071            })?;
3072        let ObjectKind::Segment { .. } = &spline_object.kind else {
3073            return Err(KclError::refactor(format!(
3074                "Object is not a segment: {spline_object:?}"
3075            )));
3076        };
3077
3078        self.mutate_ast(
3079            new_ast,
3080            spline,
3081            AstMutateCommand::EditControlPointSpline {
3082                points: points_ast,
3083                construction: ctor.construction,
3084            },
3085        )?;
3086        Ok(())
3087    }
3088
3089    fn delete_segment(
3090        &mut self,
3091        new_ast: &mut ast::Node<ast::Program>,
3092        sketch: ObjectId,
3093        segment_id: ObjectId,
3094    ) -> Result<(), KclError> {
3095        // Look up existing sketch.
3096        let sketch_id = sketch;
3097        let sketch_object = self
3098            .scene_graph
3099            .objects
3100            .get(sketch_id.0)
3101            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3102        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3103            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3104        };
3105        sketch.segments.iter().find(|o| **o == segment_id).ok_or_else(|| {
3106            KclError::refactor(format!(
3107                "Segment not found in sketch: segment={segment_id:?}, sketch={sketch:?}"
3108            ))
3109        })?;
3110        // Look up existing segment.
3111        let segment_object =
3112            self.scene_graph.objects.get(segment_id.0).ok_or_else(|| {
3113                KclError::refactor(format!("Segment not found in scene graph: segment={segment_id:?}"))
3114            })?;
3115        let ObjectKind::Segment { .. } = &segment_object.kind else {
3116            return Err(KclError::refactor(format!(
3117                "Object is not a segment, it is {}",
3118                segment_object.kind.human_friendly_kind_with_article()
3119            )));
3120        };
3121
3122        // Modify the AST to remove the segment.
3123        self.mutate_ast(new_ast, segment_id, AstMutateCommand::DeleteNode)?;
3124        Ok(())
3125    }
3126
3127    fn delete_constraint(
3128        &mut self,
3129        new_ast: &mut ast::Node<ast::Program>,
3130        sketch: ObjectId,
3131        constraint_id: ObjectId,
3132    ) -> Result<(), KclError> {
3133        // Look up existing sketch.
3134        let sketch_id = sketch;
3135        let sketch_object = self
3136            .scene_graph
3137            .objects
3138            .get(sketch_id.0)
3139            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch:?}")))?;
3140        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
3141            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
3142        };
3143        sketch
3144            .constraints
3145            .iter()
3146            .find(|o| **o == constraint_id)
3147            .ok_or_else(|| {
3148                KclError::refactor(format!(
3149                    "Constraint not found in sketch: constraint={constraint_id:?}, sketch={sketch:?}"
3150                ))
3151            })?;
3152        // Look up existing constraint.
3153        let constraint_object = self.scene_graph.objects.get(constraint_id.0).ok_or_else(|| {
3154            KclError::refactor(format!(
3155                "Constraint not found in scene graph: constraint={constraint_id:?}"
3156            ))
3157        })?;
3158        let ObjectKind::Constraint { .. } = &constraint_object.kind else {
3159            return Err(KclError::refactor(format!(
3160                "Object is not a constraint, it is {}",
3161                constraint_object.kind.human_friendly_kind_with_article()
3162            )));
3163        };
3164
3165        // Modify the AST to remove the constraint.
3166        self.mutate_ast(new_ast, constraint_id, AstMutateCommand::DeleteNode)?;
3167        Ok(())
3168    }
3169
3170    fn edit_coincident_constraint(
3171        &mut self,
3172        new_ast: &mut ast::Node<ast::Program>,
3173        constraint_id: ObjectId,
3174        segments: Vec<ConstraintSegment>,
3175    ) -> Result<(), KclError> {
3176        if segments.len() < 2 {
3177            return Err(KclError::refactor(format!(
3178                "Coincident constraint must have at least 2 inputs, got {}",
3179                segments.len()
3180            )));
3181        }
3182
3183        let segment_asts = segments
3184            .iter()
3185            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3186            .collect::<Result<Vec<_>, _>>()?;
3187
3188        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3189            elements: segment_asts,
3190            digest: None,
3191            non_code_meta: Default::default(),
3192        })));
3193
3194        self.mutate_ast(
3195            new_ast,
3196            constraint_id,
3197            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3198        )?;
3199        Ok(())
3200    }
3201
3202    fn edit_horizontal_points_constraint(
3203        &mut self,
3204        new_ast: &mut ast::Node<ast::Program>,
3205        constraint_id: ObjectId,
3206        points: Vec<ConstraintSegment>,
3207    ) -> Result<(), KclError> {
3208        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Horizontal")
3209    }
3210
3211    fn edit_vertical_points_constraint(
3212        &mut self,
3213        new_ast: &mut ast::Node<ast::Program>,
3214        constraint_id: ObjectId,
3215        points: Vec<ConstraintSegment>,
3216    ) -> Result<(), KclError> {
3217        self.edit_axis_points_constraint(new_ast, constraint_id, points, "Vertical")
3218    }
3219
3220    fn edit_axis_points_constraint(
3221        &mut self,
3222        new_ast: &mut ast::Node<ast::Program>,
3223        constraint_id: ObjectId,
3224        points: Vec<ConstraintSegment>,
3225        constraint_name: &str,
3226    ) -> Result<(), KclError> {
3227        if points.len() < 2 {
3228            return Err(KclError::refactor(format!(
3229                "{constraint_name} points constraint must have at least 2 points, got {}",
3230                points.len()
3231            )));
3232        }
3233
3234        let point_asts = points
3235            .iter()
3236            .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
3237            .collect::<Result<Vec<_>, _>>()?;
3238
3239        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3240            elements: point_asts,
3241            digest: None,
3242            non_code_meta: Default::default(),
3243        })));
3244
3245        self.mutate_ast(
3246            new_ast,
3247            constraint_id,
3248            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3249        )?;
3250        Ok(())
3251    }
3252
3253    /// updates the equalLength constraint with the given lines
3254    fn edit_equal_length_constraint(
3255        &mut self,
3256        new_ast: &mut ast::Node<ast::Program>,
3257        constraint_id: ObjectId,
3258        lines: Vec<ObjectId>,
3259    ) -> Result<(), KclError> {
3260        if lines.len() < 2 {
3261            return Err(KclError::refactor(format!(
3262                "Lines equal length constraint must have at least 2 lines, got {}",
3263                lines.len()
3264            )));
3265        }
3266
3267        let line_asts = lines
3268            .iter()
3269            .map(|line_id| {
3270                let line_object = self
3271                    .scene_graph
3272                    .objects
3273                    .get(line_id.0)
3274                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3275                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3276                    let kind = line_object.kind.human_friendly_kind_with_article();
3277                    return Err(KclError::refactor(format!(
3278                        "This constraint only works on Segments, but you selected {kind}"
3279                    )));
3280                };
3281                let Segment::Line(_) = line_segment else {
3282                    let kind = line_segment.human_friendly_kind_with_article();
3283                    return Err(KclError::refactor(format!(
3284                        "Only lines can be made equal length, but you selected {kind}"
3285                    )));
3286                };
3287
3288                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3289            })
3290            .collect::<Result<Vec<_>, _>>()?;
3291
3292        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3293            elements: line_asts,
3294            digest: None,
3295            non_code_meta: Default::default(),
3296        })));
3297
3298        self.mutate_ast(
3299            new_ast,
3300            constraint_id,
3301            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3302        )?;
3303        Ok(())
3304    }
3305
3306    /// Updates the parallel constraint with the given lines.
3307    fn edit_parallel_constraint(
3308        &mut self,
3309        new_ast: &mut ast::Node<ast::Program>,
3310        constraint_id: ObjectId,
3311        lines: Vec<ObjectId>,
3312    ) -> Result<(), KclError> {
3313        if lines.len() < 2 {
3314            return Err(KclError::refactor(format!(
3315                "Parallel constraint must have at least 2 lines, got {}",
3316                lines.len()
3317            )));
3318        }
3319
3320        let line_asts = lines
3321            .iter()
3322            .map(|line_id| {
3323                let line_object = self
3324                    .scene_graph
3325                    .objects
3326                    .get(line_id.0)
3327                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3328                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3329                    let kind = line_object.kind.human_friendly_kind_with_article();
3330                    return Err(KclError::refactor(format!(
3331                        "This constraint only works on Segments, but you selected {kind}"
3332                    )));
3333                };
3334                let Segment::Line(_) = line_segment else {
3335                    let kind = line_segment.human_friendly_kind_with_article();
3336                    return Err(KclError::refactor(format!(
3337                        "Only lines can be made parallel, but you selected {kind}"
3338                    )));
3339                };
3340
3341                get_or_insert_ast_reference(new_ast, &line_object.source.clone(), LINE_VARIABLE, None)
3342            })
3343            .collect::<Result<Vec<_>, _>>()?;
3344
3345        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3346            elements: line_asts,
3347            digest: None,
3348            non_code_meta: Default::default(),
3349        })));
3350
3351        self.mutate_ast(
3352            new_ast,
3353            constraint_id,
3354            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3355        )?;
3356        Ok(())
3357    }
3358
3359    /// Updates the equalRadius constraint with the given segments.
3360    fn edit_equal_radius_constraint(
3361        &mut self,
3362        new_ast: &mut ast::Node<ast::Program>,
3363        constraint_id: ObjectId,
3364        input: Vec<ObjectId>,
3365    ) -> Result<(), KclError> {
3366        if input.len() < 2 {
3367            return Err(KclError::refactor(format!(
3368                "equalRadius constraint must have at least 2 segments, got {}",
3369                input.len()
3370            )));
3371        }
3372
3373        let input_asts = input
3374            .iter()
3375            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
3376            .collect::<Result<Vec<_>, _>>()?;
3377
3378        let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3379            elements: input_asts,
3380            digest: None,
3381            non_code_meta: Default::default(),
3382        })));
3383
3384        self.mutate_ast(
3385            new_ast,
3386            constraint_id,
3387            AstMutateCommand::EditCallUnlabeled { arg: array_expr },
3388        )?;
3389        Ok(())
3390    }
3391
3392    async fn execute_after_edit(
3393        &mut self,
3394        ctx: &ExecutorContext,
3395        sketch: ObjectId,
3396        sketch_block_ref: AstNodeRef,
3397        new_ast: &mut ast::Node<ast::Program>,
3398        options: ExecuteAfterEditOptions,
3399    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3400        let ExecuteAfterEditOptions {
3401            segment_ids_edited,
3402            edit_kind,
3403            commit_solved_initial_guesses,
3404        } = options;
3405
3406        // Convert to string source to create real source ranges.
3407        let new_source = source_from_ast(new_ast);
3408        // Parse the new KCL source.
3409        let new_program = parse_frontend_mutation_source(
3410            &new_source,
3411            "Error parsing KCL source after editing",
3412            "No AST produced after editing",
3413        )?;
3414
3415        // Truncate after the sketch block for mock execution.
3416        let is_delete = edit_kind.is_delete();
3417        let truncated_program = {
3418            let mut truncated_program = new_program.clone();
3419            only_sketch_block(
3420                &mut truncated_program.ast,
3421                &sketch_block_ref,
3422                edit_kind.to_change_kind(),
3423            )
3424            .map_err(KclErrorWithOutputs::no_outputs)?;
3425            truncated_program
3426        };
3427
3428        // Execute.
3429        let drag_anchors = self.next_segment_drag_anchors.take().unwrap_or_default();
3430        let mock_config = MockConfig {
3431            sketch_block_id: Some(sketch),
3432            freedom_analysis: is_delete,
3433            segment_ids_edited: segment_ids_edited.clone(),
3434            drag_anchors,
3435            ..Default::default()
3436        };
3437        let outcome = ctx.run_mock(&truncated_program, &mock_config).await?;
3438
3439        // Only now, after execution has succeeded, update self.program.
3440        self.program = new_program;
3441
3442        // Uses freedom_analysis: is_delete
3443        let outcome = self.update_state_after_exec(outcome, is_delete);
3444
3445        let src_delta = if commit_solved_initial_guesses {
3446            self.commit_var_solutions_to_program(&outcome, "editing")?
3447        } else {
3448            SourceDelta { text: new_source }
3449        };
3450        let scene_graph_delta = SceneGraphDelta {
3451            new_graph: self.scene_graph_for_ui(),
3452            invalidates_ids: is_delete,
3453            new_objects: Vec::new(),
3454            exec_outcome: outcome,
3455        };
3456        Ok((src_delta, scene_graph_delta))
3457    }
3458
3459    async fn execute_after_delete_sketch(
3460        &mut self,
3461        ctx: &ExecutorContext,
3462        new_ast: &mut ast::Node<ast::Program>,
3463    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
3464        // Convert to string source to create real source ranges.
3465        let new_source = source_from_ast(new_ast);
3466        // Parse the new KCL source.
3467        let new_program = parse_frontend_mutation_source(
3468            &new_source,
3469            "Error parsing KCL source after editing",
3470            "No AST produced after editing",
3471        )?;
3472
3473        // Make sure to only set this if there are no errors.
3474        self.program = new_program.clone();
3475
3476        // We deleted the entire sketch block. It doesn't make sense to truncate
3477        // and execute only the sketch block. We execute the whole program with
3478        // a real engine.
3479
3480        // Execute.
3481        let outcome = ctx.run_with_caching(new_program).await?;
3482        let freedom_analysis_ran = true;
3483
3484        let outcome = self.update_state_after_exec(outcome, freedom_analysis_ran);
3485
3486        let src_delta = SourceDelta { text: new_source };
3487        let scene_graph_delta = SceneGraphDelta {
3488            new_graph: self.scene_graph_for_ui(),
3489            invalidates_ids: true,
3490            new_objects: Vec::new(),
3491            exec_outcome: outcome,
3492        };
3493        Ok((src_delta, scene_graph_delta))
3494    }
3495
3496    /// Map a point object id into an AST reference expression for use in
3497    /// constraints. If the point is owned by a segment (line or arc), we
3498    /// reference the appropriate property on that segment (e.g. `line1.start`,
3499    /// `arc1.center`). Otherwise we reference the point directly.
3500    fn point_id_to_ast_reference(
3501        &self,
3502        point_id: ObjectId,
3503        new_ast: &mut ast::Node<ast::Program>,
3504    ) -> Result<ast::Expr, KclError> {
3505        let point_object = self
3506            .scene_graph
3507            .objects
3508            .get(point_id.0)
3509            .ok_or_else(|| KclError::refactor(format!("Point not found: {point_id:?}")))?;
3510        let ObjectKind::Segment { segment: point_segment } = &point_object.kind else {
3511            return Err(KclError::refactor(format!("Object is not a segment: {point_object:?}")));
3512        };
3513        let Segment::Point(point) = point_segment else {
3514            return Err(KclError::refactor(format!(
3515                "Only points are currently supported: {point_object:?}"
3516            )));
3517        };
3518
3519        if let Some(owner_id) = point.owner {
3520            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3521                KclError::refactor(format!(
3522                    "Owner of point not found in scene graph: point={point_id:?}, owner={owner_id:?}"
3523                ))
3524            })?;
3525            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3526                return Err(KclError::refactor(format!(
3527                    "Owner of point is not a segment, but found {}",
3528                    owner_object.kind.human_friendly_kind_with_article()
3529                )));
3530            };
3531
3532            match owner_segment {
3533                Segment::Line(line) => {
3534                    let property = if line.start == point_id {
3535                        LINE_PROPERTY_START
3536                    } else if line.end == point_id {
3537                        LINE_PROPERTY_END
3538                    } else {
3539                        return Err(KclError::refactor(format!(
3540                            "Internal: Point is not part of owner's line segment: point={point_id:?}, line={owner_id:?}"
3541                        )));
3542                    };
3543                    get_or_insert_ast_reference(new_ast, &owner_object.source, LINE_VARIABLE, Some(property))
3544                }
3545                Segment::Arc(arc) => {
3546                    let property = if arc.start == point_id {
3547                        ARC_PROPERTY_START
3548                    } else if arc.end == point_id {
3549                        ARC_PROPERTY_END
3550                    } else if arc.center == point_id {
3551                        ARC_PROPERTY_CENTER
3552                    } else {
3553                        return Err(KclError::refactor(format!(
3554                            "Internal: Point is not part of owner's arc segment: point={point_id:?}, arc={owner_id:?}"
3555                        )));
3556                    };
3557                    get_or_insert_ast_reference(new_ast, &owner_object.source, ARC_VARIABLE, Some(property))
3558                }
3559                Segment::Circle(circle) => {
3560                    let property = if circle.start == point_id {
3561                        CIRCLE_PROPERTY_START
3562                    } else if circle.center == point_id {
3563                        CIRCLE_PROPERTY_CENTER
3564                    } else {
3565                        return Err(KclError::refactor(format!(
3566                            "Internal: Point is not part of owner's circle segment: point={point_id:?}, circle={owner_id:?}"
3567                        )));
3568                    };
3569                    get_or_insert_ast_reference(new_ast, &owner_object.source, CIRCLE_VARIABLE, Some(property))
3570                }
3571                Segment::ControlPointSpline(spline) => {
3572                    let Some(index) = spline.controls.iter().position(|id| *id == point_id) else {
3573                        return Err(KclError::refactor(format!(
3574                            "Internal: Point is not part of owner's controlPointSpline segment: point={point_id:?}, spline={owner_id:?}"
3575                        )));
3576                    };
3577                    let owner_expr =
3578                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3579                    let controls_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_CONTROLS);
3580                    Ok(create_index_expression(controls_expr, index))
3581                }
3582                _ => Err(KclError::refactor(format!(
3583                    "Internal: Owner of point is not a supported segment type for constraints: {owner_segment:?}"
3584                ))),
3585            }
3586        } else {
3587            // Standalone point.
3588            get_or_insert_ast_reference(new_ast, &point_object.source, "point", None)
3589        }
3590    }
3591
3592    fn line_id_to_ast_reference(
3593        &self,
3594        line_id: ObjectId,
3595        new_ast: &mut ast::Node<ast::Program>,
3596    ) -> Result<ast::Expr, KclError> {
3597        let line_object = self
3598            .scene_graph
3599            .objects
3600            .get(line_id.0)
3601            .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
3602        let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
3603            return Err(KclError::refactor(format!("Object is not a segment: {line_object:?}")));
3604        };
3605        let Segment::Line(line) = line_segment else {
3606            return Err(KclError::refactor(format!(
3607                "Only lines are currently supported: {line_object:?}"
3608            )));
3609        };
3610
3611        if let Some(owner_id) = line.owner {
3612            let owner_object = self.scene_graph.objects.get(owner_id.0).ok_or_else(|| {
3613                KclError::refactor(format!(
3614                    "Owner of line not found in scene graph: line={line_id:?}, owner={owner_id:?}"
3615                ))
3616            })?;
3617            let ObjectKind::Segment { segment: owner_segment } = &owner_object.kind else {
3618                return Err(KclError::refactor(format!(
3619                    "Owner of line is not a segment, but found {}",
3620                    owner_object.kind.human_friendly_kind_with_article()
3621                )));
3622            };
3623
3624            match owner_segment {
3625                Segment::ControlPointSpline(spline) => {
3626                    let Some(index) = spline
3627                        .controls
3628                        .windows(2)
3629                        .position(|window| window[0] == line.start && window[1] == line.end)
3630                    else {
3631                        return Err(KclError::refactor(format!(
3632                            "Internal: Line is not part of owner's controlPointSpline segment: line={line_id:?}, spline={owner_id:?}"
3633                        )));
3634                    };
3635                    let owner_expr =
3636                        get_or_insert_ast_reference(new_ast, &owner_object.source, CONTROL_POINT_SPLINE_FN, None)?;
3637                    let edges_expr = create_member_expression(owner_expr, CONTROL_POINT_SPLINE_PROPERTY_EDGES);
3638                    Ok(create_index_expression(edges_expr, index))
3639                }
3640                _ => Err(KclError::refactor(format!(
3641                    "Internal: Owner of line is not a supported segment type for constraints: {owner_segment:?}"
3642                ))),
3643            }
3644        } else {
3645            get_or_insert_ast_reference(new_ast, &line_object.source, "line", None)
3646        }
3647    }
3648
3649    fn coincident_segment_to_ast(
3650        &self,
3651        segment: &ConstraintSegment,
3652        new_ast: &mut ast::Node<ast::Program>,
3653    ) -> Result<ast::Expr, KclError> {
3654        match segment {
3655            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3656            ConstraintSegment::Segment(segment_id) => self.segment_id_to_constraint_ast_reference(*segment_id, new_ast),
3657        }
3658    }
3659
3660    fn segment_id_to_constraint_ast_reference(
3661        &self,
3662        segment_id: ObjectId,
3663        new_ast: &mut ast::Node<ast::Program>,
3664    ) -> Result<ast::Expr, KclError> {
3665        let segment_object = self
3666            .scene_graph
3667            .objects
3668            .get(segment_id.0)
3669            .ok_or_else(|| KclError::refactor(format!("Object not found: {segment_id:?}")))?;
3670        let ObjectKind::Segment { segment } = &segment_object.kind else {
3671            return Err(KclError::refactor(format!(
3672                "Object is not a segment, it is {}",
3673                segment_object.kind.human_friendly_kind_with_article()
3674            )));
3675        };
3676
3677        match segment {
3678            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
3679            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
3680            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None),
3681            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
3682            Segment::ControlPointSpline(_) => {
3683                get_or_insert_ast_reference(new_ast, &segment_object.source, CONTROL_POINT_SPLINE_FN, None)
3684            }
3685        }
3686    }
3687
3688    fn axis_constraint_segment_to_ast(
3689        &self,
3690        segment: &ConstraintSegment,
3691        new_ast: &mut ast::Node<ast::Program>,
3692    ) -> Result<ast::Expr, KclError> {
3693        match segment {
3694            ConstraintSegment::Origin(_) => Ok(ast_name_expr("ORIGIN".to_owned())),
3695            ConstraintSegment::Segment(point_id) => self.point_id_to_ast_reference(*point_id, new_ast),
3696        }
3697    }
3698
3699    async fn add_coincident(
3700        &mut self,
3701        sketch: ObjectId,
3702        coincident: Coincident,
3703        new_ast: &mut ast::Node<ast::Program>,
3704    ) -> Result<AstNodeRef, KclError> {
3705        let sketch_id = sketch;
3706        for segment in &coincident.segments {
3707            let ConstraintSegment::Segment(segment_id) = segment else {
3708                continue;
3709            };
3710            let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
3711                continue;
3712            };
3713            if matches!(
3714                segment_object.kind,
3715                ObjectKind::Segment {
3716                    segment: Segment::ControlPointSpline(_)
3717                }
3718            ) {
3719                return Err(KclError::refactor(
3720                    "Coincident with a full controlPointSpline is not supported yet. Constrain a control point or spline edge instead."
3721                        .to_owned(),
3722                ));
3723            }
3724        }
3725        let segment_asts = coincident
3726            .segments
3727            .iter()
3728            .map(|segment| self.coincident_segment_to_ast(segment, new_ast))
3729            .collect::<Result<Vec<_>, _>>()?;
3730        if segment_asts.len() < 2 {
3731            return Err(KclError::refactor(format!(
3732                "Coincident constraint must have at least 2 inputs, got {}",
3733                segment_asts.len()
3734            )));
3735        }
3736
3737        // Create the coincident() call using shared helper.
3738        let coincident_ast = create_coincident_ast(segment_asts);
3739
3740        // Add the line to the AST of the sketch block.
3741        let (sketch_block_ref, _) = self.mutate_ast(
3742            new_ast,
3743            sketch_id,
3744            AstMutateCommand::AddSketchBlockExprStmt { expr: coincident_ast },
3745        )?;
3746        Ok(sketch_block_ref)
3747    }
3748
3749    async fn add_distance(
3750        &mut self,
3751        sketch: ObjectId,
3752        distance: Distance,
3753        new_ast: &mut ast::Node<ast::Program>,
3754    ) -> Result<AstNodeRef, KclError> {
3755        self.add_distance_constraint(sketch, DISTANCE_FN, distance, new_ast)
3756    }
3757
3758    fn distance_constraint_ast_parts(
3759        &self,
3760        function_name: &str,
3761        distance: &Distance,
3762        new_ast: &mut ast::Node<ast::Program>,
3763    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3764        let [segment0_ast, segment1_ast] = match distance.segments.as_slice() {
3765            [pt0, pt1] => [
3766                self.coincident_segment_to_ast(pt0, new_ast)?,
3767                self.coincident_segment_to_ast(pt1, new_ast)?,
3768            ],
3769            _ => {
3770                return Err(KclError::refactor(format!(
3771                    "Distance constraint must have exactly 2 segments, got {}",
3772                    distance.segments.len()
3773                )));
3774            }
3775        };
3776
3777        let arguments = match &distance.label_position {
3778            Some(label_position) => vec![ast::LabeledArg {
3779                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3780                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3781            }],
3782            None => Default::default(),
3783        };
3784
3785        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3786            callee: ast::Node::no_src(ast_sketch2_name(function_name)),
3787            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
3788                ast::ArrayExpression {
3789                    elements: vec![segment0_ast, segment1_ast],
3790                    digest: None,
3791                    non_code_meta: Default::default(),
3792                },
3793            )))),
3794            arguments,
3795            digest: None,
3796            non_code_meta: Default::default(),
3797        })));
3798        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3799            value: ast::LiteralValue::Number {
3800                value: distance.distance.value,
3801                suffix: distance.distance.units,
3802            },
3803            raw: format_number_literal(distance.distance.value, distance.distance.units, None).map_err(|_| {
3804                KclError::refactor(format!(
3805                    "Could not format numeric suffix: {:?}",
3806                    distance.distance.units
3807                ))
3808            })?,
3809            digest: None,
3810        })));
3811
3812        Ok((call, value))
3813    }
3814
3815    fn add_distance_constraint(
3816        &mut self,
3817        sketch: ObjectId,
3818        function_name: &str,
3819        distance: Distance,
3820        new_ast: &mut ast::Node<ast::Program>,
3821    ) -> Result<AstNodeRef, KclError> {
3822        let (call, value) = self.distance_constraint_ast_parts(function_name, &distance, new_ast)?;
3823        let distance_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3824            left: call,
3825            operator: ast::BinaryOperator::Eq,
3826            right: value,
3827            digest: None,
3828        })));
3829
3830        let (sketch_block_ref, _) = self.mutate_ast(
3831            new_ast,
3832            sketch,
3833            AstMutateCommand::AddSketchBlockExprStmt { expr: distance_ast },
3834        )?;
3835        Ok(sketch_block_ref)
3836    }
3837
3838    async fn add_angle(
3839        &mut self,
3840        sketch: ObjectId,
3841        angle: Angle,
3842        new_ast: &mut ast::Node<ast::Program>,
3843    ) -> Result<AstNodeRef, KclError> {
3844        let sketch_id = sketch;
3845        let (angle_call_ast, angle_value_ast) = self.angle_constraint_ast_parts(&angle, new_ast)?;
3846        let angle_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
3847            left: angle_call_ast,
3848            operator: ast::BinaryOperator::Eq,
3849            right: angle_value_ast,
3850            digest: None,
3851        })));
3852
3853        // Add the line to the AST of the sketch block.
3854        let (sketch_block_ref, _) = self.mutate_ast(
3855            new_ast,
3856            sketch_id,
3857            AstMutateCommand::AddSketchBlockExprStmt { expr: angle_ast },
3858        )?;
3859        Ok(sketch_block_ref)
3860    }
3861
3862    fn angle_constraint_ast_parts(
3863        &self,
3864        angle: &Angle,
3865        new_ast: &mut ast::Node<ast::Program>,
3866    ) -> Result<(ast::BinaryPart, ast::BinaryPart), KclError> {
3867        let &[l0_id, l1_id] = angle.lines.as_slice() else {
3868            return Err(KclError::refactor(format!(
3869                "Angle constraint must have exactly 2 lines, got {}",
3870                angle.lines.len()
3871            )));
3872        };
3873
3874        let l0_ast = self.line_id_to_ast_reference(l0_id, new_ast)?;
3875        let l1_ast = self.line_id_to_ast_reference(l1_id, new_ast)?;
3876        let lines_ast = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
3877            elements: vec![l0_ast, l1_ast],
3878            digest: None,
3879            non_code_meta: Default::default(),
3880        })));
3881
3882        if angle.inverse == Some(true) && angle.sector.is_none() {
3883            return Err(KclError::refactor("Angle inverse requires an angle sector".to_owned()));
3884        }
3885
3886        let uses_angle_dimension = angle.sector.is_some();
3887        let mut arguments = if uses_angle_dimension {
3888            vec![ast::LabeledArg {
3889                label: Some(ast::Identifier::new(ANGLE_LINES_PARAM)),
3890                arg: lines_ast.clone(),
3891            }]
3892        } else {
3893            Default::default()
3894        };
3895
3896        if let Some(sector) = angle.sector {
3897            arguments.push(ast::LabeledArg {
3898                label: Some(ast::Identifier::new(ANGLE_SECTOR_PARAM)),
3899                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3900                    value: ast::LiteralValue::Number {
3901                        value: f64::from(sector),
3902                        suffix: NumericSuffix::None,
3903                    },
3904                    raw: sector.to_string(),
3905                    digest: None,
3906                }))),
3907            });
3908        }
3909
3910        if angle.inverse == Some(true) {
3911            arguments.push(ast::LabeledArg {
3912                label: Some(ast::Identifier::new(ANGLE_INVERSE_PARAM)),
3913                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3914                    value: ast::LiteralValue::Bool(true),
3915                    raw: true.to_string(),
3916                    digest: None,
3917                }))),
3918            });
3919        }
3920
3921        if let Some(label_position) = &angle.label_position {
3922            arguments.push(ast::LabeledArg {
3923                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
3924                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
3925            });
3926        }
3927
3928        let call = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
3929            callee: ast::Node::no_src(ast_sketch2_name(if uses_angle_dimension {
3930                ANGLE_DIMENSION_FN
3931            } else {
3932                ANGLE_FN
3933            })),
3934            unlabeled: (!uses_angle_dimension).then_some(lines_ast),
3935            arguments,
3936            digest: None,
3937            non_code_meta: Default::default(),
3938        })));
3939        let value = ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
3940            value: ast::LiteralValue::Number {
3941                value: angle.angle.value,
3942                suffix: angle.angle.units,
3943            },
3944            raw: format_number_literal(angle.angle.value, angle.angle.units, None)
3945                .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", angle.angle.units)))?,
3946            digest: None,
3947        })));
3948
3949        Ok((call, value))
3950    }
3951
3952    async fn add_tangent(
3953        &mut self,
3954        sketch: ObjectId,
3955        tangent: Tangent,
3956        new_ast: &mut ast::Node<ast::Program>,
3957    ) -> Result<AstNodeRef, KclError> {
3958        let &[seg0_id, seg1_id] = tangent.input.as_slice() else {
3959            return Err(KclError::refactor(format!(
3960                "Tangent constraint must have exactly 2 segments, got {}",
3961                tangent.input.len()
3962            )));
3963        };
3964        let sketch_id = sketch;
3965
3966        let seg0_object = self
3967            .scene_graph
3968            .objects
3969            .get(seg0_id.0)
3970            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg0_id:?}")))?;
3971        let ObjectKind::Segment { segment: seg0_segment } = &seg0_object.kind else {
3972            return Err(KclError::refactor(format!("Object is not a segment: {seg0_object:?}")));
3973        };
3974        let seg0_ast = match seg0_segment {
3975            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3976                self.segment_id_to_constraint_ast_reference(seg0_id, new_ast)?
3977            }
3978            _ => {
3979                return Err(KclError::refactor(format!(
3980                    "Tangent supports only line/arc/circle segments for now, got: {seg0_segment:?}"
3981                )));
3982            }
3983        };
3984
3985        let seg1_object = self
3986            .scene_graph
3987            .objects
3988            .get(seg1_id.0)
3989            .ok_or_else(|| KclError::refactor(format!("Segment not found: {seg1_id:?}")))?;
3990        let ObjectKind::Segment { segment: seg1_segment } = &seg1_object.kind else {
3991            return Err(KclError::refactor(format!("Object is not a segment: {seg1_object:?}")));
3992        };
3993        let seg1_ast = match seg1_segment {
3994            Segment::Line(_) | Segment::Arc(_) | Segment::Circle(_) => {
3995                self.segment_id_to_constraint_ast_reference(seg1_id, new_ast)?
3996            }
3997            _ => {
3998                return Err(KclError::refactor(format!(
3999                    "Tangent supports only line/arc/circle segments for now, got: {seg1_segment:?}"
4000                )));
4001            }
4002        };
4003
4004        let tangent_ast = create_tangent_ast(seg0_ast, seg1_ast);
4005        let (sketch_block_ref, _) = self.mutate_ast(
4006            new_ast,
4007            sketch_id,
4008            AstMutateCommand::AddSketchBlockExprStmt { expr: tangent_ast },
4009        )?;
4010        Ok(sketch_block_ref)
4011    }
4012
4013    async fn add_symmetric(
4014        &mut self,
4015        sketch: ObjectId,
4016        symmetric: Symmetric,
4017        new_ast: &mut ast::Node<ast::Program>,
4018    ) -> Result<AstNodeRef, KclError> {
4019        let &[input0_id, input1_id] = symmetric.input.as_slice() else {
4020            return Err(KclError::refactor(format!(
4021                "Symmetric constraint must have exactly 2 inputs, got {}",
4022                symmetric.input.len()
4023            )));
4024        };
4025        let sketch_id = sketch;
4026
4027        let input0_ast = self.symmetric_input_id_to_ast_reference(input0_id, new_ast)?;
4028        let input1_ast = self.symmetric_input_id_to_ast_reference(input1_id, new_ast)?;
4029        let axis_ast = self.symmetric_axis_id_to_ast_reference(symmetric.axis, new_ast)?;
4030
4031        let symmetric_ast = create_symmetric_ast(vec![input0_ast, input1_ast], axis_ast);
4032        let (sketch_block_ref, _) = self.mutate_ast(
4033            new_ast,
4034            sketch_id,
4035            AstMutateCommand::AddSketchBlockExprStmt { expr: symmetric_ast },
4036        )?;
4037        Ok(sketch_block_ref)
4038    }
4039
4040    async fn add_midpoint(
4041        &mut self,
4042        sketch: ObjectId,
4043        midpoint: Midpoint,
4044        new_ast: &mut ast::Node<ast::Program>,
4045    ) -> Result<AstNodeRef, KclError> {
4046        let sketch_id = sketch;
4047        let point_ast = self.axis_constraint_segment_to_ast(&midpoint.point, new_ast)?;
4048
4049        let segment_object = self
4050            .scene_graph
4051            .objects
4052            .get(midpoint.segment.0)
4053            .ok_or_else(|| KclError::refactor(format!("Segment not found: {:?}", midpoint.segment)))?;
4054        let ObjectKind::Segment {
4055            segment: midpoint_segment,
4056        } = &segment_object.kind
4057        else {
4058            return Err(KclError::refactor(format!(
4059                "Object must be a segment, but it was {}",
4060                segment_object.kind.human_friendly_kind_with_article()
4061            )));
4062        };
4063        let segment_ast = match midpoint_segment {
4064            Segment::Line(_) => self.line_id_to_ast_reference(midpoint.segment, new_ast)?,
4065            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, "arc", None)?,
4066            _ => {
4067                return Err(KclError::refactor(format!(
4068                    "Midpoint target must be a line or arc segment but it was {}",
4069                    midpoint_segment.human_friendly_kind_with_article()
4070                )));
4071            }
4072        };
4073
4074        let midpoint_ast = create_midpoint_ast(segment_ast, point_ast);
4075        let (sketch_block_ref, _) = self.mutate_ast(
4076            new_ast,
4077            sketch_id,
4078            AstMutateCommand::AddSketchBlockExprStmt { expr: midpoint_ast },
4079        )?;
4080        Ok(sketch_block_ref)
4081    }
4082
4083    async fn add_equal_radius(
4084        &mut self,
4085        sketch: ObjectId,
4086        equal_radius: EqualRadius,
4087        new_ast: &mut ast::Node<ast::Program>,
4088    ) -> Result<AstNodeRef, KclError> {
4089        if equal_radius.input.len() < 2 {
4090            return Err(KclError::refactor(format!(
4091                "equalRadius constraint must have at least 2 segments, got {}",
4092                equal_radius.input.len()
4093            )));
4094        }
4095
4096        let sketch_id = sketch;
4097        let input_asts = equal_radius
4098            .input
4099            .iter()
4100            .map(|segment_id| self.equal_radius_segment_id_to_ast_reference(*segment_id, new_ast))
4101            .collect::<Result<Vec<_>, _>>()?;
4102
4103        let equal_radius_ast = create_equal_radius_ast(input_asts);
4104        let (sketch_block_ref, _) = self.mutate_ast(
4105            new_ast,
4106            sketch_id,
4107            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_radius_ast },
4108        )?;
4109        Ok(sketch_block_ref)
4110    }
4111
4112    async fn add_radius(
4113        &mut self,
4114        sketch: ObjectId,
4115        radius: Radius,
4116        new_ast: &mut ast::Node<ast::Program>,
4117    ) -> Result<AstNodeRef, KclError> {
4118        let params = ArcSizeConstraintParams {
4119            points: vec![radius.arc],
4120            function_name: RADIUS_FN,
4121            value: radius.radius.value,
4122            units: radius.radius.units,
4123            label_position: radius.label_position,
4124            constraint_type_name: "Radius",
4125        };
4126        self.add_arc_size_constraint(sketch, params, new_ast).await
4127    }
4128
4129    async fn add_diameter(
4130        &mut self,
4131        sketch: ObjectId,
4132        diameter: Diameter,
4133        new_ast: &mut ast::Node<ast::Program>,
4134    ) -> Result<AstNodeRef, KclError> {
4135        let params = ArcSizeConstraintParams {
4136            points: vec![diameter.arc],
4137            function_name: DIAMETER_FN,
4138            value: diameter.diameter.value,
4139            units: diameter.diameter.units,
4140            label_position: diameter.label_position,
4141            constraint_type_name: "Diameter",
4142        };
4143        self.add_arc_size_constraint(sketch, params, new_ast).await
4144    }
4145
4146    async fn add_fixed_constraints(
4147        &mut self,
4148        sketch: ObjectId,
4149        points: Vec<FixedPoint>,
4150        new_ast: &mut ast::Node<ast::Program>,
4151    ) -> Result<AstNodeRef, KclError> {
4152        let mut sketch_block_ref = None;
4153
4154        for fixed_point in points {
4155            let point_ast = self.point_id_to_ast_reference(fixed_point.point, new_ast)?;
4156            let fixed_ast = create_fixed_point_constraint_ast(point_ast, fixed_point.position)
4157                .map_err(|err| KclError::refactor(err.to_string()))?;
4158
4159            let (sketch_ref, _) = self.mutate_ast(
4160                new_ast,
4161                sketch,
4162                AstMutateCommand::AddSketchBlockExprStmt { expr: fixed_ast },
4163            )?;
4164            sketch_block_ref = Some(sketch_ref);
4165        }
4166
4167        sketch_block_ref.ok_or_else(|| KclError::refactor("Fixed constraint requires at least one point".to_owned()))
4168    }
4169
4170    async fn add_arc_size_constraint(
4171        &mut self,
4172        sketch: ObjectId,
4173        params: ArcSizeConstraintParams,
4174        new_ast: &mut ast::Node<ast::Program>,
4175    ) -> Result<AstNodeRef, KclError> {
4176        let sketch_id = sketch;
4177
4178        // Constraint must have exactly 1 argument (arc segment)
4179        if params.points.len() != 1 {
4180            return Err(KclError::refactor(format!(
4181                "{} constraint must have exactly 1 argument (an arc segment), got {}",
4182                params.constraint_type_name,
4183                params.points.len()
4184            )));
4185        }
4186
4187        let arc_id = params.points[0];
4188        let arc_object = self
4189            .scene_graph
4190            .objects
4191            .get(arc_id.0)
4192            .ok_or_else(|| KclError::refactor(format!("Arc segment not found: {arc_id:?}")))?;
4193        let ObjectKind::Segment { segment: arc_segment } = &arc_object.kind else {
4194            return Err(KclError::refactor(format!("Object is not a segment: {arc_object:?}")));
4195        };
4196        let ref_type = match arc_segment {
4197            Segment::Arc(_) => ARC_VARIABLE,
4198            Segment::Circle(_) => CIRCLE_VARIABLE,
4199            _ => {
4200                return Err(KclError::refactor(format!(
4201                    "{} constraint argument must be an arc or circle segment, got: {arc_segment:?}",
4202                    params.constraint_type_name
4203                )));
4204            }
4205        };
4206        // Reference the arc/circle segment directly
4207        let arc_ast = get_or_insert_ast_reference(new_ast, &arc_object.source, ref_type, None)?;
4208        let arguments = match &params.label_position {
4209            Some(label_position) => vec![ast::LabeledArg {
4210                label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
4211                arg: to_ast_point2d_number(label_position).map_err(|err| KclError::refactor(err.to_string()))?,
4212            }],
4213            None => Default::default(),
4214        };
4215
4216        // Create the function call.
4217        let call_ast = ast::BinaryPart::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4218            callee: ast::Node::no_src(ast_sketch2_name(params.function_name)),
4219            unlabeled: Some(arc_ast),
4220            arguments,
4221            digest: None,
4222            non_code_meta: Default::default(),
4223        })));
4224        let constraint_ast = ast::Expr::BinaryExpression(BoxNode::new(ast::Node::no_src(ast::BinaryExpression {
4225            left: call_ast,
4226            operator: ast::BinaryOperator::Eq,
4227            right: ast::BinaryPart::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
4228                value: ast::LiteralValue::Number {
4229                    value: params.value,
4230                    suffix: params.units,
4231                },
4232                raw: format_number_literal(params.value, params.units, None)
4233                    .map_err(|_| KclError::refactor(format!("Could not format numeric suffix: {:?}", params.units)))?,
4234                digest: None,
4235            }))),
4236            digest: None,
4237        })));
4238
4239        // Add the line to the AST of the sketch block.
4240        let (sketch_block_ref, _) = self.mutate_ast(
4241            new_ast,
4242            sketch_id,
4243            AstMutateCommand::AddSketchBlockExprStmt { expr: constraint_ast },
4244        )?;
4245        Ok(sketch_block_ref)
4246    }
4247
4248    async fn add_horizontal_distance(
4249        &mut self,
4250        sketch: ObjectId,
4251        distance: Distance,
4252        new_ast: &mut ast::Node<ast::Program>,
4253    ) -> Result<AstNodeRef, KclError> {
4254        self.add_distance_constraint(sketch, HORIZONTAL_DISTANCE_FN, distance, new_ast)
4255    }
4256
4257    async fn add_vertical_distance(
4258        &mut self,
4259        sketch: ObjectId,
4260        distance: Distance,
4261        new_ast: &mut ast::Node<ast::Program>,
4262    ) -> Result<AstNodeRef, KclError> {
4263        self.add_distance_constraint(sketch, VERTICAL_DISTANCE_FN, distance, new_ast)
4264    }
4265
4266    async fn add_horizontal(
4267        &mut self,
4268        sketch: ObjectId,
4269        horizontal: Horizontal,
4270        new_ast: &mut ast::Node<ast::Program>,
4271    ) -> Result<AstNodeRef, KclError> {
4272        let sketch_id = sketch;
4273
4274        // Map the runtime objects back to variable names.
4275        let first_arg_ast = match horizontal {
4276            Horizontal::Line { line } => {
4277                let line_object = self
4278                    .scene_graph
4279                    .objects
4280                    .get(line.0)
4281                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4282                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4283                    let kind = line_object.kind.human_friendly_kind_with_article();
4284                    return Err(KclError::refactor(format!(
4285                        "This constraint only works on Segments, but you selected {kind}"
4286                    )));
4287                };
4288                let Segment::Line(_) = line_segment else {
4289                    return Err(KclError::refactor(format!(
4290                        "Only lines can be made horizontal, but you selected {}",
4291                        line_segment.human_friendly_kind_with_article(),
4292                    )));
4293                };
4294                self.line_id_to_ast_reference(line, new_ast)?
4295            }
4296            Horizontal::Points { points } => {
4297                let point_asts = points
4298                    .iter()
4299                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4300                    .collect::<Result<Vec<_>, _>>()?;
4301                ast::ArrayExpression::new(point_asts).into()
4302            }
4303        };
4304        // Create the horizontal() call using shared helper.
4305        let horizontal_ast = create_horizontal_ast(first_arg_ast);
4306
4307        // Add the line to the AST of the sketch block.
4308        let (sketch_block_ref, _) = self.mutate_ast(
4309            new_ast,
4310            sketch_id,
4311            AstMutateCommand::AddSketchBlockExprStmt { expr: horizontal_ast },
4312        )?;
4313        Ok(sketch_block_ref)
4314    }
4315
4316    async fn add_lines_equal_length(
4317        &mut self,
4318        sketch: ObjectId,
4319        lines_equal_length: LinesEqualLength,
4320        new_ast: &mut ast::Node<ast::Program>,
4321    ) -> Result<AstNodeRef, KclError> {
4322        if lines_equal_length.lines.len() < 2 {
4323            return Err(KclError::refactor(format!(
4324                "Lines equal length constraint must have at least 2 lines, got {}",
4325                lines_equal_length.lines.len()
4326            )));
4327        };
4328
4329        let sketch_id = sketch;
4330
4331        // Map the runtime objects back to variable names.
4332        let line_asts = lines_equal_length
4333            .lines
4334            .iter()
4335            .map(|line_id| {
4336                let line_object = self
4337                    .scene_graph
4338                    .objects
4339                    .get(line_id.0)
4340                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4341                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4342                    let kind = line_object.kind.human_friendly_kind_with_article();
4343                    return Err(KclError::refactor(format!(
4344                        "This constraint only works on Segments, but you selected {kind}"
4345                    )));
4346                };
4347                let Segment::Line(_) = line_segment else {
4348                    let kind = line_segment.human_friendly_kind_with_article();
4349                    return Err(KclError::refactor(format!(
4350                        "Only lines can be made equal length, but you selected {kind}"
4351                    )));
4352                };
4353
4354                self.line_id_to_ast_reference(*line_id, new_ast)
4355            })
4356            .collect::<Result<Vec<_>, _>>()?;
4357
4358        // Create the equalLength() call using shared helper.
4359        let equal_length_ast = create_equal_length_ast(line_asts);
4360
4361        // Add the constraint to the AST of the sketch block.
4362        let (sketch_block_ref, _) = self.mutate_ast(
4363            new_ast,
4364            sketch_id,
4365            AstMutateCommand::AddSketchBlockExprStmt { expr: equal_length_ast },
4366        )?;
4367        Ok(sketch_block_ref)
4368    }
4369
4370    fn equal_radius_segment_id_to_ast_reference(
4371        &mut self,
4372        segment_id: ObjectId,
4373        new_ast: &mut ast::Node<ast::Program>,
4374    ) -> Result<ast::Expr, KclError> {
4375        let segment_object = self
4376            .scene_graph
4377            .objects
4378            .get(segment_id.0)
4379            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4380        let ObjectKind::Segment { segment } = &segment_object.kind else {
4381            return Err(KclError::refactor(format!(
4382                "Object is not a segment, it was {}",
4383                segment_object.kind.human_friendly_kind_with_article()
4384            )));
4385        };
4386
4387        let ref_type = match segment {
4388            Segment::Arc(_) => ARC_VARIABLE,
4389            Segment::Circle(_) => CIRCLE_VARIABLE,
4390            _ => {
4391                return Err(KclError::refactor(format!(
4392                    "equalRadius supports only arc/circle segments, got {}",
4393                    segment.human_friendly_kind_with_article()
4394                )));
4395            }
4396        };
4397
4398        get_or_insert_ast_reference(new_ast, &segment_object.source, ref_type, None)
4399    }
4400
4401    fn symmetric_input_id_to_ast_reference(
4402        &mut self,
4403        segment_id: ObjectId,
4404        new_ast: &mut ast::Node<ast::Program>,
4405    ) -> Result<ast::Expr, KclError> {
4406        let segment_object = self
4407            .scene_graph
4408            .objects
4409            .get(segment_id.0)
4410            .ok_or_else(|| KclError::refactor(format!("Segment not found: {segment_id:?}")))?;
4411        let ObjectKind::Segment { segment } = &segment_object.kind else {
4412            return Err(KclError::refactor(format!(
4413                "Object is not a segment, it was {}",
4414                segment_object.kind.human_friendly_kind_with_article()
4415            )));
4416        };
4417
4418        match segment {
4419            Segment::Point(_) => self.point_id_to_ast_reference(segment_id, new_ast),
4420            Segment::Line(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, LINE_VARIABLE, None),
4421            Segment::Arc(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, ARC_VARIABLE, None),
4422            Segment::Circle(_) => get_or_insert_ast_reference(new_ast, &segment_object.source, CIRCLE_VARIABLE, None),
4423            Segment::ControlPointSpline(_) => Err(KclError::refactor(
4424                "Symmetric does not yet support control point splines".to_owned(),
4425            )),
4426        }
4427    }
4428
4429    fn symmetric_axis_id_to_ast_reference(
4430        &mut self,
4431        segment_id: ObjectId,
4432        new_ast: &mut ast::Node<ast::Program>,
4433    ) -> Result<ast::Expr, KclError> {
4434        let segment_object = self
4435            .scene_graph
4436            .objects
4437            .get(segment_id.0)
4438            .ok_or_else(|| KclError::refactor(format!("Axis segment not found: {segment_id:?}")))?;
4439        let ObjectKind::Segment { segment } = &segment_object.kind else {
4440            return Err(KclError::refactor(format!(
4441                "Object is not a segment, it was {}",
4442                segment_object.kind.human_friendly_kind_with_article()
4443            )));
4444        };
4445        match segment {
4446            Segment::Line(_) => self.line_id_to_ast_reference(segment_id, new_ast),
4447            _ => Err(KclError::refactor(format!(
4448                "Symmetric axis must be a line, got {}",
4449                segment.human_friendly_kind_with_article()
4450            ))),
4451        }
4452    }
4453
4454    async fn add_parallel(
4455        &mut self,
4456        sketch: ObjectId,
4457        parallel: Parallel,
4458        new_ast: &mut ast::Node<ast::Program>,
4459    ) -> Result<AstNodeRef, KclError> {
4460        if parallel.lines.len() < 2 {
4461            return Err(KclError::refactor(format!(
4462                "Parallel constraint must have at least 2 lines, got {}",
4463                parallel.lines.len()
4464            )));
4465        };
4466
4467        let sketch_id = sketch;
4468
4469        let line_asts = parallel
4470            .lines
4471            .iter()
4472            .map(|line_id| {
4473                let line_object = self
4474                    .scene_graph
4475                    .objects
4476                    .get(line_id.0)
4477                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line_id:?}")))?;
4478                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4479                    let kind = line_object.kind.human_friendly_kind_with_article();
4480                    return Err(KclError::refactor(format!(
4481                        "This constraint only works on Segments, but you selected {kind}"
4482                    )));
4483                };
4484                let Segment::Line(_) = line_segment else {
4485                    let kind = line_segment.human_friendly_kind_with_article();
4486                    return Err(KclError::refactor(format!(
4487                        "Only lines can be made parallel, but you selected {kind}"
4488                    )));
4489                };
4490
4491                self.line_id_to_ast_reference(*line_id, new_ast)
4492            })
4493            .collect::<Result<Vec<_>, _>>()?;
4494
4495        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4496            callee: ast::Node::no_src(ast_sketch2_name(LinesAtAngleKind::Parallel.to_function_name())),
4497            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4498                ast::ArrayExpression {
4499                    elements: line_asts,
4500                    digest: None,
4501                    non_code_meta: Default::default(),
4502                },
4503            )))),
4504            arguments: Default::default(),
4505            digest: None,
4506            non_code_meta: Default::default(),
4507        })));
4508
4509        let (sketch_block_ref, _) = self.mutate_ast(
4510            new_ast,
4511            sketch_id,
4512            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4513        )?;
4514        Ok(sketch_block_ref)
4515    }
4516
4517    async fn add_perpendicular(
4518        &mut self,
4519        sketch: ObjectId,
4520        perpendicular: Perpendicular,
4521        new_ast: &mut ast::Node<ast::Program>,
4522    ) -> Result<AstNodeRef, KclError> {
4523        self.add_lines_at_angle_constraint(sketch, LinesAtAngleKind::Perpendicular, perpendicular.lines, new_ast)
4524            .await
4525    }
4526
4527    async fn add_lines_at_angle_constraint(
4528        &mut self,
4529        sketch: ObjectId,
4530        angle_kind: LinesAtAngleKind,
4531        lines: Vec<ObjectId>,
4532        new_ast: &mut ast::Node<ast::Program>,
4533    ) -> Result<AstNodeRef, KclError> {
4534        let &[line0_id, line1_id] = lines.as_slice() else {
4535            return Err(KclError::refactor(format!(
4536                "{} constraint must have exactly 2 lines, got {}",
4537                angle_kind.to_function_name(),
4538                lines.len()
4539            )));
4540        };
4541
4542        let sketch_id = sketch;
4543
4544        // Map the runtime objects back to variable names.
4545        let line0_object = self
4546            .scene_graph
4547            .objects
4548            .get(line0_id.0)
4549            .ok_or_else(|| KclError::refactor(format!("Line not found: {line0_id:?}")))?;
4550        let ObjectKind::Segment { segment: line0_segment } = &line0_object.kind else {
4551            let kind = line0_object.kind.human_friendly_kind_with_article();
4552            return Err(KclError::refactor(format!(
4553                "This constraint only works on Segments, but you selected {kind}"
4554            )));
4555        };
4556        let Segment::Line(_) = line0_segment else {
4557            return Err(KclError::refactor(format!(
4558                "Only lines can be made {}, but you selected {}",
4559                angle_kind.to_function_name(),
4560                line0_segment.human_friendly_kind_with_article(),
4561            )));
4562        };
4563        let line0_ast = self.line_id_to_ast_reference(line0_id, new_ast)?;
4564
4565        let line1_object = self
4566            .scene_graph
4567            .objects
4568            .get(line1_id.0)
4569            .ok_or_else(|| KclError::refactor(format!("Line not found: {line1_id:?}")))?;
4570        let ObjectKind::Segment { segment: line1_segment } = &line1_object.kind else {
4571            let kind = line1_object.kind.human_friendly_kind_with_article();
4572            return Err(KclError::refactor(format!(
4573                "This constraint only works on Segments, but you selected {kind}"
4574            )));
4575        };
4576        let Segment::Line(_) = line1_segment else {
4577            return Err(KclError::refactor(format!(
4578                "Only lines can be made {}, but you selected {}",
4579                angle_kind.to_function_name(),
4580                line1_segment.human_friendly_kind_with_article(),
4581            )));
4582        };
4583        let line1_ast = self.line_id_to_ast_reference(line1_id, new_ast)?;
4584
4585        // Create the parallel() or perpendicular() call.
4586        let call_ast = ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
4587            callee: ast::Node::no_src(ast_sketch2_name(angle_kind.to_function_name())),
4588            unlabeled: Some(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
4589                ast::ArrayExpression {
4590                    elements: vec![line0_ast, line1_ast],
4591                    digest: None,
4592                    non_code_meta: Default::default(),
4593                },
4594            )))),
4595            arguments: Default::default(),
4596            digest: None,
4597            non_code_meta: Default::default(),
4598        })));
4599
4600        // Add the constraint to the AST of the sketch block.
4601        let (sketch_block_ref, _) = self.mutate_ast(
4602            new_ast,
4603            sketch_id,
4604            AstMutateCommand::AddSketchBlockExprStmt { expr: call_ast },
4605        )?;
4606        Ok(sketch_block_ref)
4607    }
4608
4609    async fn add_vertical(
4610        &mut self,
4611        sketch: ObjectId,
4612        vertical: Vertical,
4613        new_ast: &mut ast::Node<ast::Program>,
4614    ) -> Result<AstNodeRef, KclError> {
4615        let sketch_id = sketch;
4616
4617        let first_arg_ast = match vertical {
4618            Vertical::Line { line } => {
4619                // Map the runtime objects back to variable names.
4620                let line_object = self
4621                    .scene_graph
4622                    .objects
4623                    .get(line.0)
4624                    .ok_or_else(|| KclError::refactor(format!("Line not found: {line:?}")))?;
4625                let ObjectKind::Segment { segment: line_segment } = &line_object.kind else {
4626                    let kind = line_object.kind.human_friendly_kind_with_article();
4627                    return Err(KclError::refactor(format!(
4628                        "This constraint only works on Segments, but you selected {kind}"
4629                    )));
4630                };
4631                let Segment::Line(_) = line_segment else {
4632                    return Err(KclError::refactor(format!(
4633                        "Only lines can be made vertical, but you selected {}",
4634                        line_segment.human_friendly_kind_with_article()
4635                    )));
4636                };
4637                self.line_id_to_ast_reference(line, new_ast)?
4638            }
4639            Vertical::Points { points } => {
4640                let point_asts = points
4641                    .iter()
4642                    .map(|point| self.axis_constraint_segment_to_ast(point, new_ast))
4643                    .collect::<Result<Vec<_>, _>>()?;
4644                ast::ArrayExpression::new(point_asts).into()
4645            }
4646        };
4647        // Create the vertical() call using shared helper.
4648        let vertical_ast = create_vertical_ast(first_arg_ast);
4649
4650        // Add the line to the AST of the sketch block.
4651        let (sketch_block_ref, _) = self.mutate_ast(
4652            new_ast,
4653            sketch_id,
4654            AstMutateCommand::AddSketchBlockExprStmt { expr: vertical_ast },
4655        )?;
4656        Ok(sketch_block_ref)
4657    }
4658
4659    async fn execute_after_add_constraint(
4660        &mut self,
4661        ctx: &ExecutorContext,
4662        sketch_id: ObjectId,
4663        sketch_block_ref: AstNodeRef,
4664        new_ast: &mut ast::Node<ast::Program>,
4665    ) -> ExecResult<(SourceDelta, SceneGraphDelta)> {
4666        // Convert to string source to create real source ranges.
4667        let new_source = source_from_ast(new_ast);
4668        // Parse the new KCL source.
4669        let new_program = parse_frontend_mutation_source(
4670            &new_source,
4671            "Error parsing KCL source after adding constraint",
4672            "No AST produced after adding constraint",
4673        )?;
4674        let constraint_node_ref = find_sketch_block_added_item(&new_program.ast, &sketch_block_ref).map_err(|err| {
4675            KclErrorWithOutputs::no_outputs(KclError::refactor(format!(
4676                "Source range of new constraint not found in sketch block: {sketch_block_ref:?}; {err:?}"
4677            )))
4678        })?;
4679
4680        // Truncate after the sketch block for mock execution.
4681        // Use a clone so we don't mutate new_program yet
4682        let mut truncated_program = new_program.clone();
4683        only_sketch_block(&mut truncated_program.ast, &sketch_block_ref, ChangeKind::Add)
4684            .map_err(KclErrorWithOutputs::no_outputs)?;
4685
4686        // Execute - if this fails, we haven't modified self yet, so state is safe
4687        let outcome = ctx
4688            .run_mock(&truncated_program, &MockConfig::new_sketch_mode(sketch_id))
4689            .await?;
4690
4691        let new_object_ids = {
4692            // Extract the constraint ID from the execution outcome using source_range_to_object
4693            let constraint_id = outcome
4694                .source_range_to_object
4695                .get(&constraint_node_ref.range)
4696                .copied()
4697                .ok_or_else(|| {
4698                    KclErrorWithOutputs::from_error_outcome(
4699                        KclError::refactor(format!("Source range of constraint not found: {constraint_node_ref:?}")),
4700                        outcome.clone(),
4701                    )
4702                })?;
4703            vec![constraint_id]
4704        };
4705
4706        // Only now, after all operations succeeded, update self.program.
4707        // This ensures state is only modified if everything succeeds.
4708        self.program = new_program;
4709
4710        // Uses MockConfig::default() which has freedom_analysis: true
4711        let outcome = self.update_state_after_exec(outcome, true);
4712
4713        let src_delta = self.commit_var_solutions_to_program(&outcome, "adding constraint")?;
4714        let scene_graph_delta = SceneGraphDelta {
4715            new_graph: self.scene_graph_for_ui(),
4716            invalidates_ids: false,
4717            new_objects: new_object_ids,
4718            exec_outcome: outcome,
4719        };
4720        Ok((src_delta, scene_graph_delta))
4721    }
4722
4723    fn commit_var_solutions_to_program(&mut self, outcome: &ExecOutcome, operation: &str) -> ExecResult<SourceDelta> {
4724        let commit_failure = || {
4725            KclErrorWithOutputs::from_error_outcome(
4726                KclError::refactor(format!("Could not update KCL after {operation}.")),
4727                outcome.clone(),
4728            )
4729        };
4730
4731        let default_length_unit = self.default_length_unit();
4732        let mut settled_ast = self.program.ast.clone();
4733        let mut committed_solver_value = false;
4734        for (var_range, node_path, value) in &outcome.var_solutions {
4735            let Some(lookup) = numeric_literal_at_node_path(&settled_ast, node_path.as_ref(), *var_range) else {
4736                return Err(commit_failure());
4737            };
4738            let new_value = match &lookup {
4739                Some(current_literal) => {
4740                    if !var_solution_needs_commit(current_literal, *value, default_length_unit) {
4741                        continue;
4742                    }
4743                    preserve_var_solution_literal_style(current_literal, *value, default_length_unit)
4744                }
4745                None => {
4746                    // Bare `var` with no initial literal to compare against;
4747                    // always commit, using the module's default length unit as
4748                    // an explicit suffix so the written value carries units.
4749                    Number {
4750                        value: number_value_in_default_length_units(*value, default_length_unit),
4751                        units: default_length_unit.into(),
4752                    }
4753                }
4754            };
4755            committed_solver_value = true;
4756            let source_ref = SourceRef::Simple {
4757                range: *var_range,
4758                node_path: node_path.clone(),
4759            };
4760            mutate_ast_node_by_source_ref(
4761                &mut settled_ast,
4762                &source_ref,
4763                AstMutateCommand::EditVarInitialValue { value: new_value },
4764            )
4765            .map_err(|_| commit_failure())?;
4766        }
4767
4768        if !committed_solver_value {
4769            return Ok(SourceDelta {
4770                text: self.program.original_file_contents.clone(),
4771            });
4772        }
4773
4774        let settled_source = source_from_ast(&settled_ast);
4775        let (settled_program, errors) = Program::parse(&settled_source).map_err(|_| commit_failure())?;
4776        if !errors.is_empty() {
4777            return Err(commit_failure());
4778        }
4779        let Some(settled_program) = settled_program else {
4780            return Err(commit_failure());
4781        };
4782
4783        self.program = settled_program;
4784
4785        Ok(SourceDelta { text: settled_source })
4786    }
4787
4788    // Find constraints that reference the given segments.
4789    fn segment_will_be_deleted(&self, segment_id: ObjectId, segment_ids_set: &AhashIndexSet<ObjectId>) -> bool {
4790        if segment_ids_set.contains(&segment_id) {
4791            return true;
4792        }
4793
4794        let Some(segment_object) = self.scene_graph.objects.get(segment_id.0) else {
4795            return false;
4796        };
4797        let ObjectKind::Segment { segment } = &segment_object.kind else {
4798            return false;
4799        };
4800        let Segment::Point(point) = segment else {
4801            return false;
4802        };
4803
4804        point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id))
4805    }
4806
4807    fn remaining_constraint_segments(
4808        &self,
4809        segments: &[ConstraintSegment],
4810        segment_ids_set: &AhashIndexSet<ObjectId>,
4811    ) -> Vec<ConstraintSegment> {
4812        segments
4813            .iter()
4814            .copied()
4815            .filter(|segment| match segment {
4816                ConstraintSegment::Origin(_) => true,
4817                ConstraintSegment::Segment(segment_id) => !self.segment_will_be_deleted(*segment_id, segment_ids_set),
4818            })
4819            .collect()
4820    }
4821
4822    fn find_referenced_constraints(
4823        &self,
4824        sketch_id: ObjectId,
4825        segment_ids_set: &AhashIndexSet<ObjectId>,
4826    ) -> Result<AhashIndexSet<ObjectId>, KclError> {
4827        // Look up the sketch.
4828        let sketch_object = self
4829            .scene_graph
4830            .objects
4831            .get(sketch_id.0)
4832            .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
4833        let ObjectKind::Sketch(sketch) = &sketch_object.kind else {
4834            return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
4835        };
4836        let segment_or_owner_matches = |segment_id: ObjectId| {
4837            if segment_ids_set.contains(&segment_id) {
4838                return true;
4839            }
4840            let segment_object = self.scene_graph.objects.get(segment_id.0);
4841            if let Some(obj) = segment_object
4842                && let ObjectKind::Segment { segment } = &obj.kind
4843            {
4844                match segment {
4845                    Segment::Point(point) => point.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4846                    Segment::Line(line) => line.owner.is_some_and(|owner_id| segment_ids_set.contains(&owner_id)),
4847                    _ => false,
4848                }
4849            } else {
4850                false
4851            }
4852        };
4853        let mut constraint_ids_set = AhashIndexSet::default();
4854        for constraint_id in &sketch.constraints {
4855            let constraint_object = self
4856                .scene_graph
4857                .objects
4858                .get(constraint_id.0)
4859                .ok_or_else(|| KclError::refactor(format!("Constraint not found: {constraint_id:?}")))?;
4860            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
4861                return Err(KclError::refactor(format!(
4862                    "Object is not a constraint, it is {}",
4863                    constraint_object.kind.human_friendly_kind_with_article()
4864                )));
4865            };
4866            let depends_on_segment = match constraint {
4867                Constraint::Coincident(c) => c.segment_ids().any(segment_or_owner_matches),
4868                Constraint::Distance(d) => d.segment_ids().any(segment_or_owner_matches),
4869                Constraint::Fixed(fixed) => fixed
4870                    .points
4871                    .iter()
4872                    .any(|fixed_point| self.segment_will_be_deleted(fixed_point.point, segment_ids_set)),
4873                Constraint::Radius(r) => segment_or_owner_matches(r.arc),
4874                Constraint::Diameter(d) => segment_or_owner_matches(d.arc),
4875                Constraint::EqualRadius(equal_radius) => {
4876                    equal_radius.input.iter().copied().any(segment_or_owner_matches)
4877                }
4878                Constraint::HorizontalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4879                Constraint::VerticalDistance(d) => d.segment_ids().any(segment_or_owner_matches),
4880                Constraint::Horizontal(h) => match h {
4881                    Horizontal::Line { line } => segment_or_owner_matches(*line),
4882                    Horizontal::Points { points } => points.iter().any(|point| match point {
4883                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4884                        ConstraintSegment::Origin(_) => false,
4885                    }),
4886                },
4887                Constraint::Vertical(v) => match v {
4888                    Vertical::Line { line } => segment_or_owner_matches(*line),
4889                    Vertical::Points { points } => points.iter().any(|point| match point {
4890                        ConstraintSegment::Segment(point) => segment_or_owner_matches(*point),
4891                        ConstraintSegment::Origin(_) => false,
4892                    }),
4893                },
4894                Constraint::LinesEqualLength(lines_equal_length) => {
4895                    lines_equal_length.lines.iter().copied().any(segment_or_owner_matches)
4896                }
4897                Constraint::Midpoint(midpoint) => {
4898                    segment_or_owner_matches(midpoint.segment)
4899                        || matches!(
4900                            midpoint.point,
4901                            ConstraintSegment::Segment(point) if segment_or_owner_matches(point)
4902                        )
4903                }
4904                Constraint::Parallel(parallel) => parallel.lines.iter().copied().any(segment_or_owner_matches),
4905                Constraint::Perpendicular(perpendicular) => {
4906                    perpendicular.lines.iter().copied().any(segment_or_owner_matches)
4907                }
4908                Constraint::Angle(angle) => angle.lines.iter().copied().any(segment_or_owner_matches),
4909                Constraint::Symmetric(symmetric) => {
4910                    segment_or_owner_matches(symmetric.axis)
4911                        || symmetric.input.iter().copied().any(segment_or_owner_matches)
4912                }
4913                Constraint::Tangent(tangent) => tangent.input.iter().copied().any(segment_or_owner_matches),
4914            };
4915            if depends_on_segment {
4916                constraint_ids_set.insert(*constraint_id);
4917            }
4918        }
4919        Ok(constraint_ids_set)
4920    }
4921
4922    fn update_state_after_exec(&mut self, outcome: ExecOutcome, freedom_analysis_ran: bool) -> ExecOutcome {
4923        let mut outcome = outcome;
4924        self.solid_references = solid_references_from_variables(&self.program.ast, &outcome.variables);
4925        let mut new_objects = std::mem::take(&mut outcome.scene_objects);
4926
4927        if freedom_analysis_ran {
4928            // When freedom analysis ran, replace the cache entirely with new values
4929            // Don't merge with old values since IDs might have changed
4930            self.point_freedom_cache.clear();
4931            for new_obj in &new_objects {
4932                if let ObjectKind::Segment {
4933                    segment: kcl_api::Segment::Point(point),
4934                } = &new_obj.kind
4935                {
4936                    self.point_freedom_cache.insert(new_obj.id, point.freedom);
4937                }
4938            }
4939            add_wall_and_cap_face_objects(&mut new_objects, &outcome.artifact_graph);
4940            // Objects are already correct from the analysis, just use them as-is
4941            self.scene_graph.objects = new_objects;
4942        } else {
4943            // When freedom analysis didn't run, preserve old values and merge
4944            // Before replacing objects, extract and store freedom values from old objects
4945            for old_obj in &self.scene_graph.objects {
4946                if let ObjectKind::Segment {
4947                    segment: kcl_api::Segment::Point(point),
4948                } = &old_obj.kind
4949                {
4950                    self.point_freedom_cache.insert(old_obj.id, point.freedom);
4951                }
4952            }
4953
4954            // Update objects, preserving stored freedom values when new is Free (might be default)
4955            let mut updated_objects = Vec::with_capacity(new_objects.len());
4956            for new_obj in new_objects {
4957                let mut obj = new_obj;
4958                if let ObjectKind::Segment {
4959                    segment: kcl_api::Segment::Point(point),
4960                } = &mut obj.kind
4961                {
4962                    let new_freedom = point.freedom;
4963                    // When freedom_analysis=false, new values are defaults (Free).
4964                    // Only preserve cached values when new is Free (indicating it's a default, not from analysis).
4965                    // If new is NOT Free, use the new value (it came from somewhere else, maybe conflict detection).
4966                    // Never preserve Conflict from cache - conflicts are transient and should only be set
4967                    // when there are actually unsatisfied constraints.
4968                    match new_freedom {
4969                        Freedom::Free => {
4970                            match self.point_freedom_cache.get(&obj.id).copied() {
4971                                Some(Freedom::Conflict) => {
4972                                    // Don't preserve Conflict - conflicts are transient
4973                                    // Keep it as Free
4974                                }
4975                                Some(Freedom::Fixed) => {
4976                                    // Preserve Fixed cached value
4977                                    point.freedom = Freedom::Fixed;
4978                                }
4979                                Some(Freedom::Free) => {
4980                                    // If stored is also Free, keep Free (no change needed)
4981                                }
4982                                None => {
4983                                    // If no cached value, keep Free (default)
4984                                }
4985                            }
4986                        }
4987                        Freedom::Fixed => {
4988                            // Use new value (already set)
4989                        }
4990                        Freedom::Conflict => {
4991                            // Use new value (already set)
4992                        }
4993                    }
4994                    // Store the new freedom value (even if it's Free, so we know it was set)
4995                    self.point_freedom_cache.insert(obj.id, point.freedom);
4996                }
4997                updated_objects.push(obj);
4998            }
4999
5000            add_wall_and_cap_face_objects(&mut updated_objects, &outcome.artifact_graph);
5001            self.scene_graph.objects = updated_objects;
5002        }
5003        outcome
5004    }
5005
5006    fn mutate_ast(
5007        &mut self,
5008        ast: &mut ast::Node<ast::Program>,
5009        object_id: ObjectId,
5010        command: AstMutateCommand,
5011    ) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5012        let sketch_object = self
5013            .scene_graph
5014            .objects
5015            .get(object_id.0)
5016            .ok_or_else(|| KclError::refactor(format!("Object not found: {object_id:?}")))?;
5017        mutate_ast_node_by_source_ref(ast, &sketch_object.source, command)
5018    }
5019
5020    fn mutate_constraint_label_position(
5021        &mut self,
5022        ast: &mut ast::Node<ast::Program>,
5023        constraint_id: ObjectId,
5024        label_position: Point2d<Number>,
5025    ) -> Result<(), KclError> {
5026        let object = self
5027            .scene_graph
5028            .objects
5029            .get(constraint_id.0)
5030            .ok_or_else(|| KclError::refactor(format!("Object not found: {constraint_id:?}")))?;
5031        if !matches!(
5032            &object.kind,
5033            ObjectKind::Constraint {
5034                constraint: Constraint::Distance(_)
5035                    | Constraint::HorizontalDistance(_)
5036                    | Constraint::VerticalDistance(_)
5037                    | Constraint::Radius(_)
5038                    | Constraint::Diameter(_)
5039                    | Constraint::Angle(_),
5040            }
5041        ) {
5042            return Err(KclError::refactor(format!(
5043                "Object does not support labelPosition: {constraint_id:?}"
5044            )));
5045        }
5046
5047        let label_position = to_ast_point2d_number(&label_position)
5048            .map_err(|err| KclError::refactor(format!("Could not convert label position to AST: {err}")))?;
5049        self.mutate_ast(
5050            ast,
5051            constraint_id,
5052            AstMutateCommand::EditDistanceConstraintLabelPosition { label_position },
5053        )?;
5054        Ok(())
5055    }
5056}
5057
5058fn sketch_block_ref_from_id(scene_graph: &SceneGraph, sketch_id: ObjectId) -> Result<AstNodeRef, KclError> {
5059    // Look up existing sketch.
5060    let sketch_object = scene_graph
5061        .objects
5062        .get(sketch_id.0)
5063        .ok_or_else(|| KclError::refactor(format!("Sketch not found: {sketch_id:?}")))?;
5064    let ObjectKind::Sketch(_) = &sketch_object.kind else {
5065        return Err(KclError::refactor(format!("Object is not a sketch: {sketch_object:?}")));
5066    };
5067    expect_single_node_ref(sketch_object)
5068}
5069
5070fn expect_single_node_ref(object: &Object) -> Result<AstNodeRef, KclError> {
5071    match &object.source {
5072        SourceRef::Simple { range, node_path } => Ok(AstNodeRef {
5073            range: *range,
5074            node_path: node_path.clone(),
5075        }),
5076        SourceRef::BackTrace { ranges } => {
5077            let [range] = ranges.as_slice() else {
5078                return Err(KclError::refactor(format!(
5079                    "Expected single location in SourceRef, got {}; ranges={ranges:#?}",
5080                    ranges.len()
5081                )));
5082            };
5083            Ok(AstNodeRef {
5084                range: range.0,
5085                node_path: range.1.clone(),
5086            })
5087        }
5088    }
5089}
5090
5091/// This is a deprecated fall-back implementation. Prefer
5092/// [`only_sketch_block()`] to avoid reliance on source ranges.
5093fn only_sketch_block_from_range(
5094    ast: &mut ast::Node<ast::Program>,
5095    sketch_block_range: SourceRange,
5096    edit_kind: ChangeKind,
5097) -> Result<(), KclError> {
5098    let r1 = sketch_block_range;
5099    let matches_range = |r2: SourceRange| -> bool {
5100        // We may have added items to the sketch block, so the end may not be an
5101        // exact match.
5102        match edit_kind {
5103            ChangeKind::Add => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() <= r2.end(),
5104            // For edit, we don't know whether it grew or shrank.
5105            ChangeKind::Edit => r1.module_id() == r2.module_id() && r1.start() == r2.start(),
5106            ChangeKind::Delete => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() >= r2.end(),
5107            // No edit should be an exact match.
5108            ChangeKind::None => r1.module_id() == r2.module_id() && r1.start() == r2.start() && r1.end() == r2.end(),
5109        }
5110    };
5111    let mut found = false;
5112    for item in ast.body.iter_mut() {
5113        match item {
5114            ast::BodyItem::ImportStatement(_) => {}
5115            ast::BodyItem::ExpressionStatement(node) => {
5116                if matches_range(SourceRange::from(&*node))
5117                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.expression
5118                {
5119                    sketch_block.is_being_edited = true;
5120                    found = true;
5121                    break;
5122                }
5123            }
5124            ast::BodyItem::VariableDeclaration(node) => {
5125                if matches_range(SourceRange::from(&node.declaration.init))
5126                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.declaration.init
5127                {
5128                    sketch_block.is_being_edited = true;
5129                    found = true;
5130                    break;
5131                }
5132            }
5133            ast::BodyItem::TypeDeclaration(_) => {}
5134            ast::BodyItem::ReturnStatement(node) => {
5135                if matches_range(SourceRange::from(&node.argument))
5136                    && let ast::Expr::SketchBlock(sketch_block) = &mut node.argument
5137                {
5138                    sketch_block.is_being_edited = true;
5139                    found = true;
5140                    break;
5141                }
5142            }
5143        }
5144    }
5145    if !found {
5146        return Err(KclError::refactor(format!(
5147            "Sketch block source range not found in AST: {sketch_block_range:?}, edit_kind={edit_kind:?}"
5148        )));
5149    }
5150
5151    Ok(())
5152}
5153
5154fn only_sketch_block(
5155    ast: &mut ast::Node<ast::Program>,
5156    sketch_block_ref: &AstNodeRef,
5157    edit_kind: ChangeKind,
5158) -> Result<(), KclError> {
5159    let Some(target_node_path) = &sketch_block_ref.node_path else {
5160        #[cfg(target_arch = "wasm32")]
5161        web_sys::console::warn_1(
5162            &format!(
5163                "only_sketch_block: target sketch block ref doesn't have node path; sketch_block_ref={:#?}, edit_kind={edit_kind:#?}",
5164                sketch_block_ref
5165            )
5166            .into(),
5167        );
5168        return only_sketch_block_from_range(ast, sketch_block_ref.range, edit_kind);
5169    };
5170    struct MarkSketchBlockBeingEdited<'a> {
5171        target_node_path: &'a ast::NodePath,
5172    }
5173
5174    impl Visitor for MarkSketchBlockBeingEdited<'_> {
5175        type Break = ();
5176        type Continue = ();
5177
5178        fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5179            if let NodeMut::SketchBlock(sketch_block) = node
5180                && sketch_block.node_path.as_ref() == Some(self.target_node_path)
5181            {
5182                sketch_block.is_being_edited = true;
5183                return TraversalReturn::new_break(());
5184            }
5185            TraversalReturn::new_continue(())
5186        }
5187
5188        fn finish(&mut self, _node: NodeMut<'_>) {}
5189    }
5190
5191    let mut marker = MarkSketchBlockBeingEdited { target_node_path };
5192    let found = dfs_mut(ast, &mut marker).is_break();
5193    if !found {
5194        return Err(KclError::refactor(format!(
5195            "Sketch block node path not found in AST: {sketch_block_ref:?}, edit_kind={edit_kind:?}"
5196        )));
5197    }
5198
5199    Ok(())
5200}
5201
5202fn sketch_on_ast_expr(
5203    ast: &mut ast::Node<ast::Program>,
5204    scene_graph: &SceneGraph,
5205    solid_references: &HashMap<Uuid, SolidAstReference>,
5206    on: &Plane,
5207) -> Result<ast::Expr, KclError> {
5208    match on {
5209        Plane::Default(name) => Ok(default_plane_ast_expr(*name)),
5210        Plane::Object(object_id) => {
5211            let on_object = scene_graph
5212                .objects
5213                .get(object_id.0)
5214                .ok_or_else(|| KclError::refactor(format!("Sketch plane object not found: {object_id:?}")))?;
5215            if let Some(face_expr) = sketch_face_of_scene_object_ast_expr(ast, on_object)? {
5216                return Ok(face_expr);
5217            }
5218            get_or_insert_ast_reference(ast, &on_object.source, "plane", None)
5219        }
5220        Plane::PrimitiveFace(face) => {
5221            let solid_expr = solid_expr_for_engine_id(solid_references, face.solid_id).ok_or_else(|| {
5222                KclError::refactor(format!(
5223                    "Could not resolve a KCL solid for selected primitive face: solid_id={}",
5224                    face.solid_id
5225                ))
5226            })?;
5227            let face_id_expr = create_face_id_ast(solid_expr.clone(), face.index);
5228            Ok(create_face_of_ast(solid_expr, face_id_expr))
5229        }
5230    }
5231}
5232
5233fn solid_references_from_variables(
5234    ast: &ast::Node<ast::Program>,
5235    variables: &IndexMap<String, KclValueView>,
5236) -> HashMap<Uuid, SolidAstReference> {
5237    let mut references = HashMap::new();
5238
5239    // In-place operations such as shell reuse their input solid's engine ID.
5240    // Later variables overwrite earlier references so mutations target the result.
5241    for item in &ast.body {
5242        let ast::BodyItem::VariableDeclaration(declaration) = item else {
5243            continue;
5244        };
5245        let name = &declaration.declaration.id.name;
5246        let Some(value) = variables.get(name) else {
5247            continue;
5248        };
5249
5250        match value {
5251            KclValueView::Solid { value } => {
5252                references.insert(
5253                    value.id,
5254                    SolidAstReference {
5255                        variable_name: name.clone(),
5256                        output_index: None,
5257                    },
5258                );
5259            }
5260            KclValueView::Tuple { value } | KclValueView::HomArray { value } => {
5261                for (output_index, entry) in value.iter().enumerate() {
5262                    if let KclValueView::Solid { value } = entry {
5263                        references.insert(
5264                            value.id,
5265                            SolidAstReference {
5266                                variable_name: name.clone(),
5267                                output_index: Some(output_index),
5268                            },
5269                        );
5270                    }
5271                }
5272            }
5273            _ => {}
5274        }
5275    }
5276
5277    references
5278}
5279
5280fn solid_expr_for_engine_id(solid_references: &HashMap<Uuid, SolidAstReference>, solid_id: Uuid) -> Option<ast::Expr> {
5281    let reference = solid_references.get(&solid_id)?;
5282    let solid_expr = ast_name_expr(reference.variable_name.clone());
5283    Some(indexed_solid_expr_for_sweep_output(solid_expr, reference.output_index))
5284}
5285
5286fn sketch_face_of_scene_object_ast_expr(
5287    ast: &mut ast::Node<ast::Program>,
5288    on_object: &kcl_api::Object,
5289) -> Result<Option<ast::Expr>, KclError> {
5290    match &on_object.kind {
5291        ObjectKind::Wall(wall) => {
5292            let solid_ref = get_or_insert_ast_reference(
5293                ast,
5294                &source_ref_from_source_ref_range(&wall.source.solid),
5295                "solid",
5296                None,
5297            )?;
5298            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5299                return Err(KclError::refactor(format!(
5300                    "Could not resolve solid reference for selected wall: artifact_id={:?}",
5301                    on_object.artifact_id
5302                )));
5303            };
5304            let solid_expr = indexed_solid_expr_for_sweep_output(
5305                ast_name_expr(solid_name_expr.name.name.clone()),
5306                wall.solid_output_index,
5307            );
5308            let sweep_ref = get_or_insert_ast_reference(
5309                ast,
5310                &source_ref_from_source_ref_range(&wall.source.sweep),
5311                "solid",
5312                None,
5313            )?;
5314            let ast::Expr::Name(sweep_name_expr) = sweep_ref else {
5315                return Err(KclError::refactor(format!(
5316                    "Could not resolve sweep reference for selected wall: artifact_id={:?}",
5317                    on_object.artifact_id
5318                )));
5319            };
5320            let sweep_name = sweep_name_expr.name.name.clone();
5321            let segment_ref = get_or_insert_ast_reference(
5322                ast,
5323                &source_ref_from_source_ref_range(&wall.source.segment),
5324                LINE_VARIABLE,
5325                None,
5326            )?;
5327
5328            let face_expr = if let Some(region_name) = region_name_from_sweep_variable(ast, &sweep_name).or_else(|| {
5329                wall.source
5330                    .path
5331                    .as_ref()
5332                    .and_then(|path_source| region_name_from_path_source(ast, path_source))
5333            }) {
5334                let ast::Expr::Name(segment_name_expr) = segment_ref else {
5335                    return Err(KclError::refactor(format!(
5336                        "Could not resolve source segment reference for selected region wall: artifact_id={:?}",
5337                        on_object.artifact_id
5338                    )));
5339                };
5340                create_member_expression(
5341                    create_member_expression(ast_name_expr(region_name), "tags"),
5342                    &segment_name_expr.name.name,
5343                )
5344            } else {
5345                segment_ref
5346            };
5347
5348            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5349        }
5350        ObjectKind::Cap(cap) => {
5351            let solid_ref =
5352                get_or_insert_ast_reference(ast, &source_ref_from_source_ref_range(&cap.source.solid), "solid", None)?;
5353            let ast::Expr::Name(solid_name_expr) = solid_ref else {
5354                return Err(KclError::refactor(format!(
5355                    "Could not resolve solid reference for selected cap: artifact_id={:?}",
5356                    on_object.artifact_id
5357                )));
5358            };
5359            let solid_expr = indexed_solid_expr_for_sweep_output(
5360                ast_name_expr(solid_name_expr.name.name.clone()),
5361                cap.solid_output_index,
5362            );
5363            // TODO: change this to explicit tag references with tagStart/tagEnd mutations
5364            let face_expr = match cap.kind {
5365                kcl_api::CapKind::Start => ast_name_expr("START".to_owned()),
5366                kcl_api::CapKind::End => ast_name_expr("END".to_owned()),
5367            };
5368
5369            Ok(Some(create_face_of_ast(solid_expr, face_expr)))
5370        }
5371        _ => Ok(None),
5372    }
5373}
5374
5375fn indexed_solid_expr_for_sweep_output(solid_expr: ast::Expr, solid_output_index: Option<usize>) -> ast::Expr {
5376    match solid_output_index {
5377        Some(output_index) => create_index_expression(solid_expr, output_index),
5378        None => solid_expr,
5379    }
5380}
5381
5382fn source_ref_from_source_ref_range(source: &SourceRefRange) -> SourceRef {
5383    SourceRef::Simple {
5384        range: source.range,
5385        node_path: source.node_path.clone(),
5386    }
5387}
5388
5389fn region_name_from_path_source(ast: &ast::Node<ast::Program>, path_source: &SourceRefRange) -> Option<String> {
5390    let source_ref = source_ref_from_source_ref_range(path_source);
5391    let candidate = variable_name_containing_source_ref(ast, &source_ref)?;
5392    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5393        return None;
5394    };
5395    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5396        return None;
5397    };
5398    if region_call.callee.name.name != "region" {
5399        return None;
5400    }
5401    Some(candidate)
5402}
5403
5404fn downstream_composite_code_ref_for_source(artifact_graph: &ArtifactGraph, source_id: ArtifactId) -> Option<&CodeRef> {
5405    let mut current_id = source_id;
5406    let mut current_composite = None;
5407    let mut visited = HashSet::new();
5408
5409    while visited.insert(current_id) {
5410        let next_composite_id = downstream_composite_id_for_solid_source(artifact_graph, current_id);
5411
5412        let Some(composite_id) = next_composite_id else {
5413            break;
5414        };
5415        let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id) else {
5416            break;
5417        };
5418
5419        current_id = composite.id;
5420        current_composite = Some(composite);
5421
5422        if !composite.consumed {
5423            break;
5424        }
5425    }
5426
5427    current_composite.map(|composite| &composite.code_ref)
5428}
5429
5430fn downstream_composite_id_for_solid_source(
5431    artifact_graph: &ArtifactGraph,
5432    source_id: ArtifactId,
5433) -> Option<ArtifactId> {
5434    // Source is a path, find its solid.
5435    if let Some(Artifact::Path(path)) = artifact_graph.get(&source_id)
5436        && let Some(composite_id) = path.composite_solid_id
5437        && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5438        && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5439    {
5440        return Some(composite_id);
5441    }
5442
5443    // Source is a sweep, find its path -> then find the solid
5444    for artifact in artifact_graph.values() {
5445        if let Artifact::Path(path) = artifact
5446            && path.sweep_id == Some(source_id)
5447            && let Some(composite_id) = path.composite_solid_id
5448            && let Some(Artifact::CompositeSolid(composite)) = artifact_graph.get(&composite_id)
5449            && composite_contains_path_input(&composite.solid_ids, &composite.tool_ids, path.id, path.solid2d_id)
5450        {
5451            return Some(composite_id);
5452        }
5453    }
5454
5455    // Source is a solid, find its downstream solid.
5456    artifact_graph.values().find_map(|artifact| {
5457        let Artifact::CompositeSolid(composite) = artifact else {
5458            return None;
5459        };
5460        composite_contains_input(&composite.solid_ids, &composite.tool_ids, source_id).then_some(composite.id)
5461    })
5462}
5463
5464fn composite_contains_path_input(
5465    solid_ids: &[ArtifactId],
5466    tool_ids: &[ArtifactId],
5467    path_id: ArtifactId,
5468    solid2d_id: Option<ArtifactId>,
5469) -> bool {
5470    composite_contains_input(solid_ids, tool_ids, path_id)
5471        || solid2d_id.is_some_and(|solid2d_id| composite_contains_input(solid_ids, tool_ids, solid2d_id))
5472}
5473
5474fn composite_contains_input(solid_ids: &[ArtifactId], tool_ids: &[ArtifactId], input_id: ArtifactId) -> bool {
5475    solid_ids.contains(&input_id) || tool_ids.contains(&input_id)
5476}
5477
5478fn code_ref_source_ref_range(code_ref: &CodeRef) -> SourceRefRange {
5479    let node_path = (!code_ref.node_path.is_empty()).then(|| code_ref.node_path.clone());
5480    SourceRefRange {
5481        range: code_ref.range,
5482        node_path,
5483    }
5484}
5485
5486fn solid_output_index_for_sweep(
5487    artifact_graph: &ArtifactGraph,
5488    sweep_id: ArtifactId,
5489    sweep_code_ref: &CodeRef,
5490) -> Option<usize> {
5491    // Constituent sweeps are implementation details of one composite body,
5492    // even when cloning gives them the same CodeRef as the composite root.
5493    if downstream_composite_id_for_solid_source(artifact_graph, sweep_id).is_some() {
5494        return None;
5495    }
5496
5497    let sibling_sweeps = artifact_graph
5498        .values()
5499        .filter_map(|artifact| match artifact {
5500            Artifact::Sweep(sweep)
5501                if sweep.code_ref.range == sweep_code_ref.range
5502                    && sweep.code_ref.node_path == sweep_code_ref.node_path =>
5503            {
5504                Some(sweep)
5505            }
5506            _ => None,
5507        })
5508        .collect::<Vec<_>>();
5509
5510    if sibling_sweeps.len() <= 1 {
5511        return None;
5512    }
5513
5514    sibling_sweeps
5515        .iter()
5516        .position(|sibling_sweep| sibling_sweep.id == sweep_id)
5517}
5518
5519fn add_wall_and_cap_face_objects(scene_objects: &mut Vec<kcl_api::Object>, artifact_graph: &ArtifactGraph) {
5520    let mut existing_artifact_ids = scene_objects
5521        .iter()
5522        .map(|object| object.artifact_id)
5523        .collect::<HashSet<_>>();
5524
5525    for artifact in artifact_graph.values() {
5526        match artifact {
5527            Artifact::Wall(wall) => {
5528                if existing_artifact_ids.contains(&wall.id) {
5529                    continue;
5530                }
5531
5532                let Some(segment) = artifact_graph.get(&wall.seg_id).and_then(|artifact| match artifact {
5533                    Artifact::Segment(segment) => Some(segment),
5534                    _ => None,
5535                }) else {
5536                    continue;
5537                };
5538                let Some(sweep) = artifact_graph.get(&wall.sweep_id).and_then(|artifact| match artifact {
5539                    Artifact::Sweep(sweep) => Some(sweep),
5540                    _ => None,
5541                }) else {
5542                    continue;
5543                };
5544                let source_segment = segment
5545                    .original_seg_id
5546                    .and_then(|original_seg_id| artifact_graph.get(&original_seg_id))
5547                    .and_then(|artifact| match artifact {
5548                        Artifact::Segment(segment) => Some(segment),
5549                        _ => None,
5550                    })
5551                    .unwrap_or(segment);
5552                let solid_code_ref =
5553                    downstream_composite_code_ref_for_source(artifact_graph, wall.sweep_id).unwrap_or(&sweep.code_ref);
5554                let path_code_ref = artifact_graph
5555                    .get(&segment.path_id)
5556                    .or_else(|| sweep.path_id.and_then(|path_id| artifact_graph.get(&path_id)))
5557                    .and_then(|artifact| match artifact {
5558                        Artifact::Path(path) => Some(&path.code_ref),
5559                        _ => None,
5560                    });
5561                let source = WallSource {
5562                    solid: code_ref_source_ref_range(solid_code_ref),
5563                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5564                    path: path_code_ref.map(code_ref_source_ref_range),
5565                    segment: code_ref_source_ref_range(&source_segment.code_ref),
5566                };
5567                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5568                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5569                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5570                    .flatten();
5571                let object_source = source_ref_from_source_ref_range(&source.solid);
5572                let id = ObjectId(scene_objects.len());
5573                scene_objects.push(kcl_api::Object {
5574                    id,
5575                    kind: ObjectKind::Wall(kcl_api::Wall {
5576                        id,
5577                        source,
5578                        solid_output_index,
5579                    }),
5580                    label: Default::default(),
5581                    comments: Default::default(),
5582                    artifact_id: wall.id,
5583                    source: object_source,
5584                });
5585                existing_artifact_ids.insert(wall.id);
5586            }
5587            Artifact::Cap(cap) => {
5588                if existing_artifact_ids.contains(&cap.id) {
5589                    continue;
5590                }
5591
5592                let Some(sweep) = artifact_graph.get(&cap.sweep_id).and_then(|artifact| match artifact {
5593                    Artifact::Sweep(sweep) => Some(sweep),
5594                    _ => None,
5595                }) else {
5596                    continue;
5597                };
5598                let id = ObjectId(scene_objects.len());
5599                let kind = match cap.sub_type {
5600                    CapSubType::Start => kcl_api::CapKind::Start,
5601                    CapSubType::End => kcl_api::CapKind::End,
5602                };
5603                let solid_code_ref =
5604                    downstream_composite_code_ref_for_source(artifact_graph, cap.sweep_id).unwrap_or(&sweep.code_ref);
5605                let source = CapSource {
5606                    solid: code_ref_source_ref_range(solid_code_ref),
5607                    sweep: code_ref_source_ref_range(&sweep.code_ref),
5608                };
5609                let solid_output_index = (solid_code_ref.range == sweep.code_ref.range
5610                    && solid_code_ref.node_path == sweep.code_ref.node_path)
5611                    .then(|| solid_output_index_for_sweep(artifact_graph, sweep.id, &sweep.code_ref))
5612                    .flatten();
5613                let object_source = source_ref_from_source_ref_range(&source.solid);
5614                scene_objects.push(kcl_api::Object {
5615                    id,
5616                    kind: ObjectKind::Cap(kcl_api::Cap {
5617                        id,
5618                        kind,
5619                        source,
5620                        solid_output_index,
5621                    }),
5622                    label: Default::default(),
5623                    comments: Default::default(),
5624                    artifact_id: cap.id,
5625                    source: object_source,
5626                });
5627                existing_artifact_ids.insert(cap.id);
5628            }
5629            _ => {}
5630        }
5631    }
5632}
5633
5634fn default_plane_ast_expr(name: crate::engine::PlaneName) -> ast::Expr {
5635    use crate::engine::PlaneName;
5636
5637    match name {
5638        PlaneName::Xy => ast_name_expr("XY".to_owned()),
5639        PlaneName::Xz => ast_name_expr("XZ".to_owned()),
5640        PlaneName::Yz => ast_name_expr("YZ".to_owned()),
5641        PlaneName::NegXy => negated_plane_ast_expr("XY"),
5642        PlaneName::NegXz => negated_plane_ast_expr("XZ"),
5643        PlaneName::NegYz => negated_plane_ast_expr("YZ"),
5644    }
5645}
5646
5647fn negated_plane_ast_expr(name: &str) -> ast::Expr {
5648    ast::Expr::UnaryExpression(BoxNode::new(ast::UnaryExpression::new(
5649        ast::UnaryOperator::Neg,
5650        ast::BinaryPart::Name(BoxNode::new(ast_name(name.to_owned()))),
5651    )))
5652}
5653
5654fn create_face_of_ast(solid_expr: ast::Expr, face_expr: ast::Expr) -> ast::Expr {
5655    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5656        callee: ast::Node::no_src(ast_sketch2_name("faceOf")),
5657        unlabeled: Some(solid_expr),
5658        arguments: vec![ast::LabeledArg {
5659            label: Some(ast::Identifier::new("face")),
5660            arg: face_expr,
5661        }],
5662        digest: None,
5663        non_code_meta: Default::default(),
5664    })))
5665}
5666
5667fn create_face_id_ast(solid_expr: ast::Expr, index: usize) -> ast::Expr {
5668    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
5669        callee: ast::Node::no_src(ast_sketch2_name("faceId")),
5670        unlabeled: Some(solid_expr),
5671        arguments: vec![ast::LabeledArg {
5672            label: Some(ast::Identifier::new("index")),
5673            arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
5674                ast::NumericLiteral {
5675                    value: index as f64,
5676                    suffix: NumericSuffix::None,
5677                    raw: index.to_string(),
5678                    digest: None,
5679                },
5680            )))),
5681        }],
5682        digest: None,
5683        non_code_meta: Default::default(),
5684    })))
5685}
5686
5687fn region_name_from_sweep_variable(ast: &ast::Node<ast::Program>, sweep_variable_name: &str) -> Option<String> {
5688    let ast::Definition::Variable(sweep_decl) = ast.get_variable(sweep_variable_name)? else {
5689        return None;
5690    };
5691    let ast::Expr::CallExpressionKw(sweep_call) = &sweep_decl.init else {
5692        return None;
5693    };
5694    if !matches!(
5695        sweep_call.callee.name.name.as_str(),
5696        "extrude" | "revolve" | "sweep" | "loft"
5697    ) {
5698        return None;
5699    }
5700    let ast::Expr::Name(region_name_expr) = sweep_call.unlabeled.as_ref()? else {
5701        return None;
5702    };
5703    let candidate = region_name_expr.name.name.clone();
5704    let ast::Definition::Variable(region_decl) = ast.get_variable(&candidate)? else {
5705        return None;
5706    };
5707    let ast::Expr::CallExpressionKw(region_call) = &region_decl.init else {
5708        return None;
5709    };
5710    if region_call.callee.name.name != "region" {
5711        return None;
5712    }
5713    Some(candidate)
5714}
5715
5716/// Return the AST expression referencing the variable at the given source ref.
5717/// If no such variable exists, insert a new variable declaration with the given
5718/// prefix.
5719///
5720/// This may return a complex expression referencing properties of the variable
5721/// (e.g., `line1.start`).
5722fn get_or_insert_ast_reference(
5723    ast: &mut ast::Node<ast::Program>,
5724    source_ref: &SourceRef,
5725    prefix: &str,
5726    property: Option<&str>,
5727) -> Result<ast::Expr, KclError> {
5728    let command = AstMutateCommand::AddVariableDeclaration {
5729        prefix: prefix.to_owned(),
5730    };
5731    let ret = match mutate_ast_node_by_source_ref(ast, source_ref, command) {
5732        Ok((_, ret)) => ret,
5733        Err(err) => {
5734            if let Some(var_name) = variable_name_containing_source_ref(ast, source_ref) {
5735                AstMutateCommandReturn::Name(var_name)
5736            } else {
5737                return Err(err);
5738            }
5739        }
5740    };
5741    let AstMutateCommandReturn::Name(var_name) = ret else {
5742        return Err(KclError::refactor(
5743            "Expected variable name returned from AddVariableDeclaration".to_owned(),
5744        ));
5745    };
5746    let var_expr = ast::Expr::Name(BoxNode::new(ast::Name::new(&var_name)));
5747    let Some(property) = property else {
5748        // No property; just return the variable name.
5749        return Ok(var_expr);
5750    };
5751
5752    Ok(create_member_expression(var_expr, property))
5753}
5754
5755fn variable_name_containing_source_ref(ast: &ast::Node<ast::Program>, source_ref: &SourceRef) -> Option<String> {
5756    let source_range = match source_ref {
5757        SourceRef::Simple { range, .. } => *range,
5758        SourceRef::BackTrace { ranges } => {
5759            let [range] = ranges.as_slice() else {
5760                return None;
5761            };
5762            range.0
5763        }
5764    };
5765    ast.body.iter().find_map(|item| {
5766        let ast::BodyItem::VariableDeclaration(var_decl) = item else {
5767            return None;
5768        };
5769        let init_range = SourceRange::from(&var_decl.declaration.init);
5770        let source_is_inside_init = init_range.module_id() == source_range.module_id()
5771            && init_range.start() <= source_range.start()
5772            && source_range.end() <= init_range.end();
5773        if matches!(&var_decl.declaration.init, ast::Expr::SketchBlock(_))
5774            && init_range != source_range
5775            && source_is_inside_init
5776        {
5777            return None;
5778        }
5779        source_is_inside_init.then(|| var_decl.name().to_owned())
5780    })
5781}
5782
5783fn mutate_ast_node_by_source_ref(
5784    ast: &mut ast::Node<ast::Program>,
5785    source_ref: &SourceRef,
5786    command: AstMutateCommand,
5787) -> Result<(AstNodeRef, AstMutateCommandReturn), KclError> {
5788    let (source_range, node_path) = match source_ref {
5789        SourceRef::Simple { range, node_path } => (*range, node_path.clone()),
5790        SourceRef::BackTrace { ranges } => {
5791            let [range] = ranges.as_slice() else {
5792                return Err(KclError::refactor(format!(
5793                    "Expected single source ref, got {}; ranges={ranges:#?}",
5794                    ranges.len(),
5795                )));
5796            };
5797            (range.0, range.1.clone())
5798        }
5799    };
5800    let mut context = AstMutateContext {
5801        source_range,
5802        node_path,
5803        command,
5804        defined_names_stack: Default::default(),
5805    };
5806    let control = dfs_mut(ast, &mut context);
5807    match control {
5808        ControlFlow::Continue(_) => Err(KclError::refactor(
5809            "Could not find the KCL source for this edit. Try reloading the app, or update from code.".to_owned(),
5810        )),
5811        ControlFlow::Break(break_value) => break_value,
5812    }
5813}
5814
5815#[derive(Debug)]
5816struct AstMutateContext {
5817    source_range: SourceRange,
5818    node_path: Option<ast::NodePath>,
5819    command: AstMutateCommand,
5820    defined_names_stack: Vec<HashSet<String>>,
5821}
5822
5823#[derive(Debug)]
5824#[allow(clippy::large_enum_variant)]
5825enum AstMutateCommand {
5826    /// Add an expression statement to the sketch block.
5827    AddSketchBlockExprStmt {
5828        expr: ast::Expr,
5829    },
5830    /// Add a variable declaration to the sketch block (e.g. `line1 = line(...)`).
5831    AddSketchBlockVarDecl {
5832        prefix: String,
5833        expr: ast::Expr,
5834    },
5835    AddVariableDeclaration {
5836        prefix: String,
5837    },
5838    EditPoint {
5839        at: ast::Expr,
5840    },
5841    EditLine {
5842        start: ast::Expr,
5843        end: ast::Expr,
5844        construction: Option<bool>,
5845    },
5846    EditArc {
5847        start: ast::Expr,
5848        end: ast::Expr,
5849        center: ast::Expr,
5850        direction: Option<ArcDirection>,
5851        construction: Option<bool>,
5852    },
5853    EditCircle {
5854        start: ast::Expr,
5855        center: ast::Expr,
5856        construction: Option<bool>,
5857    },
5858    EditControlPointSpline {
5859        points: ast::Expr,
5860        construction: Option<bool>,
5861    },
5862    EditConstraintValue {
5863        value: ast::BinaryPart,
5864    },
5865    EditAngleConstraint {
5866        call: ast::BinaryPart,
5867        value: ast::BinaryPart,
5868    },
5869    EditDistanceConstraint {
5870        call: ast::BinaryPart,
5871        value: ast::BinaryPart,
5872    },
5873    EditDistanceConstraintLabelPosition {
5874        label_position: ast::Expr,
5875    },
5876    EditCallUnlabeled {
5877        arg: ast::Expr,
5878    },
5879    EditVarInitialValue {
5880        value: Number,
5881    },
5882    DeleteNode,
5883}
5884
5885impl AstMutateCommand {
5886    fn needs_defined_names_stack(&self) -> bool {
5887        matches!(
5888            self,
5889            AstMutateCommand::AddSketchBlockVarDecl { .. } | AstMutateCommand::AddVariableDeclaration { .. }
5890        )
5891    }
5892}
5893
5894#[derive(Debug)]
5895enum AstMutateCommandReturn {
5896    None,
5897    Name(String),
5898}
5899
5900#[derive(Debug, Clone)]
5901struct AstNodeRef {
5902    range: SourceRange,
5903    node_path: Option<ast::NodePath>,
5904}
5905
5906impl<T> From<&ast::Node<T>> for AstNodeRef {
5907    fn from(value: &ast::Node<T>) -> Self {
5908        AstNodeRef {
5909            range: value.into(),
5910            node_path: value.node_path.clone(),
5911        }
5912    }
5913}
5914
5915impl From<&ast::BodyItem> for AstNodeRef {
5916    fn from(value: &ast::BodyItem) -> Self {
5917        match value {
5918            ast::BodyItem::ImportStatement(node) => AstNodeRef {
5919                range: node.into(),
5920                node_path: node.node_path.clone(),
5921            },
5922            ast::BodyItem::ExpressionStatement(node) => AstNodeRef {
5923                range: node.into(),
5924                node_path: node.node_path.clone(),
5925            },
5926            ast::BodyItem::VariableDeclaration(node) => AstNodeRef {
5927                range: node.into(),
5928                node_path: node.node_path.clone(),
5929            },
5930            ast::BodyItem::TypeDeclaration(node) => AstNodeRef {
5931                range: node.into(),
5932                node_path: node.node_path.clone(),
5933            },
5934            ast::BodyItem::ReturnStatement(node) => AstNodeRef {
5935                range: node.into(),
5936                node_path: node.node_path.clone(),
5937            },
5938        }
5939    }
5940}
5941
5942impl From<&ast::Expr> for AstNodeRef {
5943    fn from(value: &ast::Expr) -> Self {
5944        AstNodeRef {
5945            range: SourceRange::from(value),
5946            node_path: value.node_path().cloned(),
5947        }
5948    }
5949}
5950
5951impl From<&AstMutateContext> for AstNodeRef {
5952    fn from(value: &AstMutateContext) -> Self {
5953        AstNodeRef {
5954            range: value.source_range,
5955            node_path: value.node_path.clone(),
5956        }
5957    }
5958}
5959
5960impl TryFrom<&NodeMut<'_>> for AstNodeRef {
5961    type Error = crate::walk::AstNodeError;
5962
5963    fn try_from(value: &NodeMut<'_>) -> Result<Self, Self::Error> {
5964        Ok(AstNodeRef {
5965            range: SourceRange::try_from(value)?,
5966            node_path: value.try_into()?,
5967        })
5968    }
5969}
5970
5971impl From<AstNodeRef> for SourceRange {
5972    fn from(value: AstNodeRef) -> Self {
5973        value.range
5974    }
5975}
5976
5977impl Visitor for AstMutateContext {
5978    type Break = Result<(AstNodeRef, AstMutateCommandReturn), KclError>;
5979    type Continue = ();
5980
5981    fn visit(&mut self, node: NodeMut<'_>) -> TraversalReturn<Self::Break, Self::Continue> {
5982        filter_and_process(self, node)
5983    }
5984
5985    fn finish(&mut self, node: NodeMut<'_>) {
5986        match &node {
5987            NodeMut::Program(_) | NodeMut::SketchBlock(_) => {
5988                self.defined_names_stack.pop();
5989            }
5990            _ => {}
5991        }
5992    }
5993}
5994
5995fn filter_and_process(
5996    ctx: &mut AstMutateContext,
5997    node: NodeMut,
5998) -> TraversalReturn<Result<(AstNodeRef, AstMutateCommandReturn), KclError>> {
5999    let Ok(node_range) = SourceRange::try_from(&node) else {
6000        // Nodes that can't be converted to a range aren't interesting.
6001        return TraversalReturn::new_continue(());
6002    };
6003    // If we're adding a variable declaration, we need to look at variable
6004    // declaration expressions to see if it already has a variable, before
6005    // continuing. The variable declaration's source range won't match the
6006    // target; its init expression will.
6007    if let NodeMut::VariableDeclaration(var_decl) = &node {
6008        let expr_range = SourceRange::from(&var_decl.declaration.init);
6009        let expr_node_path = var_decl.declaration.init.node_path();
6010        if source_ref_matches(ctx, expr_range, expr_node_path) {
6011            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6012                // We found the variable declaration expression. It doesn't need
6013                // to be added.
6014                return TraversalReturn::new_break(Ok((
6015                    AstNodeRef::from(&**var_decl),
6016                    AstMutateCommandReturn::Name(var_decl.name().to_owned()),
6017                )));
6018            }
6019            if let AstMutateCommand::DeleteNode = &ctx.command {
6020                // We found the variable declaration. Delete the variable along
6021                // with the segment.
6022                return TraversalReturn {
6023                    mutate_body_item: MutateBodyItem::Delete,
6024                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6025                };
6026            }
6027        }
6028    }
6029    // Similar thing with expression statement. We need to look at the
6030    // expression inside it.
6031    if let NodeMut::ExpressionStatement(expr_stmt) = &node {
6032        let expr_range = SourceRange::from(&expr_stmt.expression);
6033        let expr_node_path = expr_stmt.expression.node_path();
6034        if source_ref_matches(ctx, expr_range, expr_node_path) {
6035            if let AstMutateCommand::AddVariableDeclaration { .. } = &ctx.command {
6036                // We found the node wrapped in an expression statement. Process
6037                // the statement.
6038                let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6039                    return TraversalReturn::new_continue(());
6040                };
6041                return process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)));
6042            }
6043            if let AstMutateCommand::DeleteNode = &ctx.command {
6044                // We found the node wrapped in an expression statement. Delete
6045                // the whole statement.
6046                return TraversalReturn {
6047                    mutate_body_item: MutateBodyItem::Delete,
6048                    control_flow: ControlFlow::Break(Ok((AstNodeRef::from(&*ctx), AstMutateCommandReturn::None))),
6049                };
6050            }
6051        }
6052    }
6053
6054    if ctx.command.needs_defined_names_stack() {
6055        if let NodeMut::Program(program) = &node {
6056            ctx.defined_names_stack.push(find_defined_names(*program));
6057        } else if let NodeMut::SketchBlock(block) = &node {
6058            ctx.defined_names_stack.push(find_defined_names(&block.body));
6059        }
6060    }
6061
6062    // Make sure the node matches the source ref.
6063    let node_path = <Option<ast::NodePath>>::try_from(&node).ok().flatten();
6064    if !source_ref_matches(ctx, node_range, node_path.as_ref()) {
6065        return TraversalReturn::new_continue(());
6066    }
6067    let Ok(node_ref) = AstNodeRef::try_from(&node) else {
6068        return TraversalReturn::new_continue(());
6069    };
6070    process(ctx, node).map_break(|result| result.map(|cmd_return| (node_ref, cmd_return)))
6071}
6072
6073fn source_ref_matches(ctx: &AstMutateContext, node_range: SourceRange, node_path: Option<&ast::NodePath>) -> bool {
6074    match &ctx.node_path {
6075        Some(target) => Some(target) == node_path,
6076        None => node_range == ctx.source_range,
6077    }
6078}
6079
6080fn is_angle_constraint_call_name(name: &str) -> bool {
6081    matches!(name, ANGLE_FN | ANGLE_DIMENSION_FN)
6082}
6083
6084fn is_distance_constraint_call_name(name: &str) -> bool {
6085    matches!(name, DISTANCE_FN | HORIZONTAL_DISTANCE_FN | VERTICAL_DISTANCE_FN)
6086}
6087
6088fn is_constraint_call_name(name: &str) -> bool {
6089    matches!(
6090        name,
6091        DISTANCE_FN
6092            | HORIZONTAL_DISTANCE_FN
6093            | VERTICAL_DISTANCE_FN
6094            | RADIUS_FN
6095            | DIAMETER_FN
6096            | ANGLE_FN
6097            | ANGLE_DIMENSION_FN
6098    )
6099}
6100
6101fn constraint_supports_label_position(part: &mut ast::BinaryPart) -> Option<&mut BoxNode<CallExpressionKw>> {
6102    if let ast::BinaryPart::CallExpressionKw(call) = part
6103        && is_constraint_call_name(call.callee.name.name.as_str())
6104    {
6105        Some(call)
6106    } else {
6107        None
6108    }
6109}
6110
6111fn process(ctx: &AstMutateContext, node: NodeMut) -> TraversalReturn<Result<AstMutateCommandReturn, KclError>> {
6112    match &ctx.command {
6113        AstMutateCommand::AddSketchBlockExprStmt { expr } => {
6114            if let NodeMut::SketchBlock(sketch_block) = node {
6115                sketch_block
6116                    .body
6117                    .items
6118                    .push(ast::BodyItem::ExpressionStatement(ast::Node {
6119                        inner: ast::ExpressionStatement {
6120                            expression: expr.clone(),
6121                            digest: None,
6122                        },
6123                        start: Default::default(),
6124                        end: Default::default(),
6125                        module_id: Default::default(),
6126                        node_path: None,
6127                        outer_attrs: Default::default(),
6128                        pre_comments: Default::default(),
6129                        comment_start: Default::default(),
6130                    }));
6131                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6132            }
6133        }
6134        AstMutateCommand::AddSketchBlockVarDecl { prefix, expr } => {
6135            if let NodeMut::SketchBlock(sketch_block) = node {
6136                let empty_defined_names = HashSet::new();
6137                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6138                let Ok(name) = next_free_name(prefix, defined_names) else {
6139                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6140                };
6141                sketch_block
6142                    .body
6143                    .items
6144                    .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
6145                        ast::VariableDeclaration::new(
6146                            ast::VariableDeclarator::new(&name, expr.clone()),
6147                            ast::ItemVisibility::Default,
6148                            ast::VariableKind::Const,
6149                        ),
6150                    ))));
6151                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(name)));
6152            }
6153        }
6154        AstMutateCommand::AddVariableDeclaration { prefix } => {
6155            if let NodeMut::VariableDeclaration(inner) = node {
6156                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::Name(inner.name().to_owned())));
6157            }
6158            if let NodeMut::ExpressionStatement(expr_stmt) = node {
6159                let empty_defined_names = HashSet::new();
6160                let defined_names = ctx.defined_names_stack.last().unwrap_or(&empty_defined_names);
6161                let Ok(name) = next_free_name(prefix, defined_names) else {
6162                    // TODO: Return an error instead?
6163                    return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6164                };
6165                let mutate_node =
6166                    ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(ast::VariableDeclaration::new(
6167                        ast::VariableDeclarator::new(&name, expr_stmt.expression.clone()),
6168                        ast::ItemVisibility::Default,
6169                        ast::VariableKind::Const,
6170                    ))));
6171                return TraversalReturn {
6172                    mutate_body_item: MutateBodyItem::Mutate(Box::new(mutate_node)),
6173                    control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::Name(name))),
6174                };
6175            }
6176        }
6177        AstMutateCommand::EditPoint { at } => {
6178            if let NodeMut::CallExpressionKw(call) = node {
6179                if call.callee.name.name != POINT_FN {
6180                    return TraversalReturn::new_continue(());
6181                }
6182                // Update the arguments.
6183                for labeled_arg in &mut call.arguments {
6184                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(POINT_AT_PARAM) {
6185                        labeled_arg.arg = at.clone();
6186                    }
6187                }
6188                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6189            }
6190        }
6191        AstMutateCommand::EditLine {
6192            start,
6193            end,
6194            construction,
6195        } => {
6196            if let NodeMut::CallExpressionKw(call) = node {
6197                if call.callee.name.name != LINE_FN {
6198                    return TraversalReturn::new_continue(());
6199                }
6200                // Update the arguments.
6201                for labeled_arg in &mut call.arguments {
6202                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_START_PARAM) {
6203                        labeled_arg.arg = start.clone();
6204                    }
6205                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(LINE_END_PARAM) {
6206                        labeled_arg.arg = end.clone();
6207                    }
6208                }
6209                // Handle construction kwarg
6210                if let Some(construction_value) = construction {
6211                    let construction_exists = call
6212                        .arguments
6213                        .iter()
6214                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6215                    if *construction_value {
6216                        // Add or update construction=true
6217                        if construction_exists {
6218                            // Update existing construction kwarg
6219                            for labeled_arg in &mut call.arguments {
6220                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6221                                    labeled_arg.arg =
6222                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6223                                            value: ast::LiteralValue::Bool(true),
6224                                            raw: "true".to_string(),
6225                                            digest: None,
6226                                        })));
6227                                }
6228                            }
6229                        } else {
6230                            // Add new construction kwarg
6231                            call.arguments.push(ast::LabeledArg {
6232                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6233                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6234                                    value: ast::LiteralValue::Bool(true),
6235                                    raw: "true".to_string(),
6236                                    digest: None,
6237                                }))),
6238                            });
6239                        }
6240                    } else {
6241                        // Remove construction kwarg if it exists
6242                        call.arguments
6243                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6244                    }
6245                }
6246                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6247            }
6248        }
6249        AstMutateCommand::EditArc {
6250            start,
6251            end,
6252            center,
6253            direction,
6254            construction,
6255        } => {
6256            if let NodeMut::CallExpressionKw(call) = node {
6257                if call.callee.name.name != ARC_FN {
6258                    return TraversalReturn::new_continue(());
6259                }
6260                // Update the arguments.
6261                for labeled_arg in &mut call.arguments {
6262                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_START_PARAM) {
6263                        labeled_arg.arg = start.clone();
6264                    }
6265                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_END_PARAM) {
6266                        labeled_arg.arg = end.clone();
6267                    }
6268                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_CENTER_PARAM) {
6269                        labeled_arg.arg = center.clone();
6270                    }
6271                }
6272                // Handle direction kwarg
6273                if let Some(direction_value) = direction {
6274                    let direction_exists = call
6275                        .arguments
6276                        .iter()
6277                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM));
6278                    if direction_value.is_clockwise() {
6279                        let direction_ast = ast::Expr::Name(BoxNode::new(ast::Name::new(ARC_DIRECTION_CW_NAME)));
6280                        if direction_exists {
6281                            // Update existing direction kwarg
6282                            for labeled_arg in &mut call.arguments {
6283                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(ARC_DIRECTION_PARAM) {
6284                                    labeled_arg.arg = direction_ast.clone();
6285                                }
6286                            }
6287                        } else {
6288                            // Add new direction kwarg
6289                            call.arguments.push(ast::LabeledArg {
6290                                label: Some(ast::Identifier::new(ARC_DIRECTION_PARAM)),
6291                                arg: direction_ast,
6292                            });
6293                        }
6294                    } else {
6295                        // Remove direction kwarg if it exists since
6296                        // counterclockwise is the default
6297                        call.arguments
6298                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(ARC_DIRECTION_PARAM));
6299                    }
6300                }
6301                // Handle construction kwarg
6302                if let Some(construction_value) = construction {
6303                    let construction_exists = call
6304                        .arguments
6305                        .iter()
6306                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6307                    if *construction_value {
6308                        // Add or update construction=true
6309                        if construction_exists {
6310                            // Update existing construction kwarg
6311                            for labeled_arg in &mut call.arguments {
6312                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6313                                    labeled_arg.arg =
6314                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6315                                            value: ast::LiteralValue::Bool(true),
6316                                            raw: "true".to_string(),
6317                                            digest: None,
6318                                        })));
6319                                }
6320                            }
6321                        } else {
6322                            // Add new construction kwarg
6323                            call.arguments.push(ast::LabeledArg {
6324                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6325                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6326                                    value: ast::LiteralValue::Bool(true),
6327                                    raw: "true".to_string(),
6328                                    digest: None,
6329                                }))),
6330                            });
6331                        }
6332                    } else {
6333                        // Remove construction kwarg if it exists
6334                        call.arguments
6335                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6336                    }
6337                }
6338                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6339            }
6340        }
6341        AstMutateCommand::EditCircle {
6342            start,
6343            center,
6344            construction,
6345        } => {
6346            if let NodeMut::CallExpressionKw(call) = node {
6347                if call.callee.name.name != CIRCLE_FN {
6348                    return TraversalReturn::new_continue(());
6349                }
6350                // Update the arguments.
6351                for labeled_arg in &mut call.arguments {
6352                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_START_PARAM) {
6353                        labeled_arg.arg = start.clone();
6354                    }
6355                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CIRCLE_CENTER_PARAM) {
6356                        labeled_arg.arg = center.clone();
6357                    }
6358                }
6359                // Handle construction kwarg
6360                if let Some(construction_value) = construction {
6361                    let construction_exists = call
6362                        .arguments
6363                        .iter()
6364                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6365                    if *construction_value {
6366                        if construction_exists {
6367                            // Update existing construction kwarg
6368                            for labeled_arg in &mut call.arguments {
6369                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6370                                    labeled_arg.arg =
6371                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6372                                            value: ast::LiteralValue::Bool(true),
6373                                            raw: "true".to_string(),
6374                                            digest: None,
6375                                        })));
6376                                }
6377                            }
6378                        } else {
6379                            // Add new construction kwarg
6380                            call.arguments.push(ast::LabeledArg {
6381                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6382                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6383                                    value: ast::LiteralValue::Bool(true),
6384                                    raw: "true".to_string(),
6385                                    digest: None,
6386                                }))),
6387                            });
6388                        }
6389                    } else {
6390                        // Remove construction kwarg if it exists
6391                        call.arguments
6392                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6393                    }
6394                }
6395                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6396            }
6397        }
6398        AstMutateCommand::EditControlPointSpline { points, construction } => {
6399            if let NodeMut::CallExpressionKw(call) = node {
6400                if call.callee.name.name != CONTROL_POINT_SPLINE_FN {
6401                    return TraversalReturn::new_continue(());
6402                }
6403                for labeled_arg in &mut call.arguments {
6404                    if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONTROL_POINT_SPLINE_POINTS_PARAM)
6405                    {
6406                        labeled_arg.arg = points.clone();
6407                    }
6408                }
6409                // Handle construction kwarg
6410                if let Some(construction_value) = construction {
6411                    let construction_exists = call
6412                        .arguments
6413                        .iter()
6414                        .any(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM));
6415                    if *construction_value {
6416                        if construction_exists {
6417                            for labeled_arg in &mut call.arguments {
6418                                if labeled_arg.label.as_ref().map(|id| id.name.as_str()) == Some(CONSTRUCTION_PARAM) {
6419                                    labeled_arg.arg =
6420                                        ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6421                                            value: ast::LiteralValue::Bool(true),
6422                                            raw: "true".to_string(),
6423                                            digest: None,
6424                                        })));
6425                                }
6426                            }
6427                        } else {
6428                            call.arguments.push(ast::LabeledArg {
6429                                label: Some(ast::Identifier::new(CONSTRUCTION_PARAM)),
6430                                arg: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal {
6431                                    value: ast::LiteralValue::Bool(true),
6432                                    raw: "true".to_string(),
6433                                    digest: None,
6434                                }))),
6435                            });
6436                        }
6437                    } else {
6438                        call.arguments
6439                            .retain(|arg| arg.label.as_ref().map(|id| id.name.as_str()) != Some(CONSTRUCTION_PARAM));
6440                    }
6441                }
6442                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6443            }
6444        }
6445        AstMutateCommand::EditConstraintValue { value } => {
6446            if let NodeMut::BinaryExpression(binary_expr) = node {
6447                let left_is_constraint = matches!(
6448                    &binary_expr.left,
6449                    ast::BinaryPart::CallExpressionKw(call) if is_constraint_call_name(call.callee.name.name.as_str())
6450                );
6451                if left_is_constraint {
6452                    binary_expr.right = value.clone();
6453                } else {
6454                    binary_expr.left = value.clone();
6455                }
6456
6457                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6458            }
6459        }
6460        AstMutateCommand::EditAngleConstraint { call, value } => {
6461            if let NodeMut::BinaryExpression(binary_expr) = node {
6462                let left_is_angle = matches!(
6463                    &binary_expr.left,
6464                    ast::BinaryPart::CallExpressionKw(existing_call)
6465                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6466                );
6467                let right_is_angle = matches!(
6468                    &binary_expr.right,
6469                    ast::BinaryPart::CallExpressionKw(existing_call)
6470                        if is_angle_constraint_call_name(existing_call.callee.name.name.as_str())
6471                );
6472
6473                match (left_is_angle, right_is_angle) {
6474                    (true, _) => {
6475                        binary_expr.left = call.clone();
6476                        binary_expr.right = value.clone();
6477                    }
6478                    (false, true) => {
6479                        binary_expr.left = value.clone();
6480                        binary_expr.right = call.clone();
6481                    }
6482                    (false, false) => return TraversalReturn::new_continue(()),
6483                }
6484
6485                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6486            }
6487        }
6488        AstMutateCommand::EditDistanceConstraint { call, value } => {
6489            if let NodeMut::BinaryExpression(binary_expr) = node {
6490                let left_is_distance = matches!(
6491                    &binary_expr.left,
6492                    ast::BinaryPart::CallExpressionKw(existing_call)
6493                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6494                );
6495                let right_is_distance = matches!(
6496                    &binary_expr.right,
6497                    ast::BinaryPart::CallExpressionKw(existing_call)
6498                        if is_distance_constraint_call_name(existing_call.callee.name.name.as_str())
6499                );
6500
6501                match (left_is_distance, right_is_distance) {
6502                    (true, _) => {
6503                        binary_expr.left = call.clone();
6504                        binary_expr.right = value.clone();
6505                    }
6506                    (false, true) => {
6507                        binary_expr.left = value.clone();
6508                        binary_expr.right = call.clone();
6509                    }
6510                    (false, false) => return TraversalReturn::new_continue(()),
6511                }
6512
6513                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6514            }
6515        }
6516        AstMutateCommand::EditDistanceConstraintLabelPosition { label_position } => {
6517            if let NodeMut::BinaryExpression(binary_expr) = node {
6518                let call = if let Some(call) = constraint_supports_label_position(&mut binary_expr.left) {
6519                    call
6520                } else if let Some(call) = constraint_supports_label_position(&mut binary_expr.right) {
6521                    call
6522                } else {
6523                    return TraversalReturn::new_continue(());
6524                };
6525
6526                if let Some(label_arg) = call
6527                    .arguments
6528                    .iter_mut()
6529                    .find(|arg| arg.label.as_ref().map(|id| id.name.as_str()) == Some(LABEL_POSITION_PARAM))
6530                {
6531                    label_arg.arg = label_position.clone();
6532                } else {
6533                    call.arguments.push(ast::LabeledArg {
6534                        label: Some(ast::Identifier::new(LABEL_POSITION_PARAM)),
6535                        arg: label_position.clone(),
6536                    });
6537                }
6538
6539                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6540            }
6541        }
6542        AstMutateCommand::EditCallUnlabeled { arg } => {
6543            if let NodeMut::CallExpressionKw(call) = node {
6544                call.unlabeled = Some(arg.clone());
6545                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6546            }
6547        }
6548        AstMutateCommand::EditVarInitialValue { value } => {
6549            // We target the SketchVar itself (matched by NodePath) rather than
6550            // the inner NumericLiteral so we can also write back into vars that
6551            // were declared without an initial value (e.g. bare `var`).
6552            if let NodeMut::SketchVar(sketch_var) = node {
6553                let Ok(literal) = to_source_number(*value) else {
6554                    return TraversalReturn::new_break(Err(KclError::refactor(format!(
6555                        "Could not convert number to AST literal: {:?}",
6556                        *value
6557                    ))));
6558                };
6559                sketch_var.initial = Some(BoxNode::new(ast::Node::no_src(literal)));
6560                return TraversalReturn::new_break(Ok(AstMutateCommandReturn::None));
6561            }
6562        }
6563        AstMutateCommand::DeleteNode => {
6564            return TraversalReturn {
6565                mutate_body_item: MutateBodyItem::Delete,
6566                control_flow: ControlFlow::Break(Ok(AstMutateCommandReturn::None)),
6567            };
6568        }
6569    }
6570    TraversalReturn::new_continue(())
6571}
6572
6573struct FindSketchBlockSourceRange {
6574    /// The source range of the sketch block before mutation.
6575    target_before_mutation: SourceRange,
6576    /// The source range of the sketch block's last body item after mutation. We
6577    /// need to use a [Cell] since the [crate::walk::Visitor] trait requires a
6578    /// shared reference.
6579    found: Cell<Option<AstNodeRef>>,
6580}
6581
6582impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockSourceRange {
6583    type Error = crate::front::Error;
6584
6585    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6586        let Ok(node_range) = SourceRange::try_from(&node) else {
6587            return Ok(true);
6588        };
6589
6590        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6591            if node_range.module_id() == self.target_before_mutation.module_id()
6592                && node_range.start() == self.target_before_mutation.start()
6593                // End shouldn't match since we added something.
6594                && node_range.end() >= self.target_before_mutation.end()
6595            {
6596                self.found.set(sketch_block.body.items.last().map(|item| match item {
6597                    // For declarations like `circle1 = circle(...)`, use
6598                    // the init expression range so lookup in source_range_to_object
6599                    // matches the segment source range.
6600                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6601                    _ => AstNodeRef::from(item),
6602                }));
6603                return Ok(false);
6604            } else {
6605                // We found a different sketch block. No need to descend into
6606                // its children since sketch blocks cannot be nested.
6607                return Ok(true);
6608            }
6609        }
6610
6611        for child in node.children().iter() {
6612            if !child.visit(*self)? {
6613                return Ok(false);
6614            }
6615        }
6616
6617        Ok(true)
6618    }
6619}
6620
6621struct FindSketchBlockByNodePath {
6622    /// The Node Path of the sketch block before mutation.
6623    target_node_path: ast::NodePath,
6624    /// The ref of the sketch block's last body item after mutation. We need to
6625    /// use a [Cell] since the [crate::walk::Visitor] trait requires a shared
6626    /// reference.
6627    found: Cell<Option<AstNodeRef>>,
6628}
6629
6630impl<'a> crate::walk::Visitor<'a> for &FindSketchBlockByNodePath {
6631    type Error = crate::front::Error;
6632
6633    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6634        let Ok(node_path) = <Option<ast::NodePath>>::try_from(&node) else {
6635            return Ok(true);
6636        };
6637
6638        if let crate::walk::Node::SketchBlock(sketch_block) = node {
6639            if let Some(node_path) = node_path
6640                && node_path == self.target_node_path
6641            {
6642                self.found.set(sketch_block.body.items.last().map(|item| match item {
6643                    // For declarations like `circle1 = circle(...)`, use
6644                    // the init expression range so lookup in source_range_to_object
6645                    // matches the segment source range.
6646                    ast::BodyItem::VariableDeclaration(node) => AstNodeRef::from(&node.declaration.init),
6647                    _ => AstNodeRef::from(item),
6648                }));
6649
6650                return Ok(false);
6651            } else {
6652                // We found a different sketch block. No need to descend into
6653                // its children since sketch blocks cannot be nested.
6654                return Ok(true);
6655            }
6656        }
6657
6658        for child in node.children().iter() {
6659            if !child.visit(*self)? {
6660                return Ok(false);
6661            }
6662        }
6663
6664        Ok(true)
6665    }
6666}
6667
6668/// After adding an item to a sketch block, find the sketch block, and get the
6669/// source range of the added item. We assume that the added item is the last
6670/// item in the sketch block and that the sketch block's source range has grown,
6671/// but not moved from its starting offset.
6672///
6673/// TODO: Do we need to format *before* mutation in case formatting moves the
6674/// sketch block forward?
6675fn find_sketch_block_added_item(
6676    ast: &ast::Node<ast::Program>,
6677    sketch_block_before_mutation: &AstNodeRef,
6678) -> Result<AstNodeRef, KclError> {
6679    if let Some(node_path) = &sketch_block_before_mutation.node_path {
6680        let find = FindSketchBlockByNodePath {
6681            target_node_path: node_path.clone(),
6682            found: Cell::new(None),
6683        };
6684        let node = crate::walk::Node::from(ast);
6685        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6686        find.found.into_inner().ok_or_else(|| {
6687            KclError::refactor(format!(
6688                "Node ID after mutation not found for Node ID before mutation: {node_path:?}"
6689            ))
6690        })
6691    } else {
6692        // No NodePath. Fall back to legacy source range.
6693        let find = FindSketchBlockSourceRange {
6694            target_before_mutation: sketch_block_before_mutation.range,
6695            found: Cell::new(None),
6696        };
6697        let node = crate::walk::Node::from(ast);
6698        node.visit(&find).map_err(|err| KclError::refactor(err.msg))?;
6699        find.found.into_inner().ok_or_else(|| KclError::refactor(
6700            format!("Source range after mutation not found for range before mutation: {sketch_block_before_mutation:?}; Did you try formatting (i.e. call recast) before calling this?"),
6701        ))
6702    }
6703}
6704
6705fn format_kcl_error_message(prefix: &str, error: &KclError) -> String {
6706    let message = error.message().trim();
6707    let message = if message.is_empty() {
6708        "unknown parse error"
6709    } else {
6710        message
6711    };
6712
6713    format!("{prefix}: {message}")
6714}
6715
6716fn parse_frontend_mutation_source(source: &str, parse_error_prefix: &str, no_ast_message: &str) -> ExecResult<Program> {
6717    let (program, errors) = Program::parse(source).map_err(|err| {
6718        KclErrorWithOutputs::no_outputs(KclError::refactor(format_kcl_error_message(parse_error_prefix, &err)))
6719    })?;
6720    if !errors.is_empty() {
6721        return Err(KclErrorWithOutputs::no_outputs(KclError::refactor(
6722            format_compilation_issues(parse_error_prefix, &errors),
6723        )));
6724    }
6725
6726    program.ok_or_else(|| KclErrorWithOutputs::no_outputs(KclError::refactor(no_ast_message.to_owned())))
6727}
6728
6729fn format_compilation_issues(prefix: &str, issues: &[CompilationIssue]) -> String {
6730    let Some(first_issue) = issues
6731        .iter()
6732        .find(|issue| issue.severity.is_err())
6733        .or_else(|| issues.first())
6734    else {
6735        return prefix.to_owned();
6736    };
6737
6738    let message = first_issue.message.trim();
6739    let message = if message.is_empty() {
6740        "unknown parse error"
6741    } else {
6742        message
6743    };
6744
6745    if issues.len() > 1 {
6746        format!("{prefix}: {message} (+{} more)", issues.len() - 1)
6747    } else {
6748        format!("{prefix}: {message}")
6749    }
6750}
6751
6752fn source_from_ast(ast: &ast::Node<ast::Program>) -> String {
6753    // TODO: Don't duplicate this from lib.rs Program.
6754    ast.recast_top(&Default::default(), 0)
6755}
6756
6757struct FindNumericLiteral {
6758    target: SourceRange,
6759    found: Cell<Option<ast::NumericLiteral>>,
6760}
6761
6762impl<'a> crate::walk::Visitor<'a> for &FindNumericLiteral {
6763    type Error = crate::front::Error;
6764
6765    fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
6766        let Ok(node_range) = SourceRange::try_from(&node) else {
6767            return Ok(true);
6768        };
6769
6770        if node_range == self.target
6771            && let crate::walk::Node::NumericLiteral(literal) = node
6772        {
6773            self.found.set(Some(literal.inner.clone()));
6774            return Ok(false);
6775        }
6776
6777        for child in node.children().iter() {
6778            if !child.visit(*self)? {
6779                return Ok(false);
6780            }
6781        }
6782
6783        Ok(true)
6784    }
6785}
6786
6787fn numeric_literal_at_source_range(ast: &ast::Node<ast::Program>, target: SourceRange) -> Option<ast::NumericLiteral> {
6788    let find = FindNumericLiteral {
6789        target,
6790        found: Cell::new(None),
6791    };
6792    let node = crate::walk::Node::from(ast);
6793    node.visit(&find).ok()?;
6794    find.found.into_inner()
6795}
6796
6797struct FindSketchVarInitialByNodePath<'a> {
6798    target: &'a ast::NodePath,
6799    sketch_var_found: Cell<bool>,
6800    initial_literal: Cell<Option<ast::NumericLiteral>>,
6801}
6802
6803impl<'a, 'b> crate::walk::Visitor<'b> for &FindSketchVarInitialByNodePath<'a> {
6804    type Error = crate::front::Error;
6805
6806    fn visit_node(&self, node: crate::walk::Node<'b>) -> anyhow::Result<bool, Self::Error> {
6807        if let crate::walk::Node::SketchVar(sketch_var) = node
6808            && sketch_var.node_path.as_ref() == Some(self.target)
6809        {
6810            self.sketch_var_found.set(true);
6811            if let Some(initial) = &sketch_var.initial {
6812                self.initial_literal.set(Some(initial.inner.clone()));
6813            }
6814            return Ok(false);
6815        }
6816
6817        for child in node.children().iter() {
6818            if !child.visit(*self)? {
6819                return Ok(false);
6820            }
6821        }
6822
6823        Ok(true)
6824    }
6825}
6826
6827/// Locate the source `var` declaration corresponding to a sketch-var solution.
6828///
6829/// The outer [`Option`] distinguishes "no matching target" (commit must fail)
6830/// from "target found." The inner [`Option`] is the initial numeric literal of
6831/// the [`SketchVar`], if any; bare `var` declarations return `Some(None)`.
6832///
6833/// When `node_path` is `None` (e.g. for older outcomes that predate the
6834/// node-path propagation), this falls back to source-range matching, which
6835/// can break under whitespace shifts elsewhere in the file.
6836fn numeric_literal_at_node_path(
6837    ast: &ast::Node<ast::Program>,
6838    node_path: Option<&ast::NodePath>,
6839    source_range: SourceRange,
6840) -> Option<Option<ast::NumericLiteral>> {
6841    let Some(node_path) = node_path else {
6842        let message = "numeric_literal_at_node_path: missing node_path on var solution; falling back to source-range lookup, which can fail under whitespace shifts";
6843        #[cfg(target_arch = "wasm32")]
6844        web_sys::console::warn_1(&message.into());
6845        #[cfg(not(target_arch = "wasm32"))]
6846        eprintln!("WARNING: {message}");
6847        return numeric_literal_at_source_range(ast, source_range).map(Some);
6848    };
6849    let find = FindSketchVarInitialByNodePath {
6850        target: node_path,
6851        sketch_var_found: Cell::new(false),
6852        initial_literal: Cell::new(None),
6853    };
6854    let node = crate::walk::Node::from(ast);
6855    node.visit(&find).ok()?;
6856    if !find.sketch_var_found.get() {
6857        return None;
6858    }
6859    Some(find.initial_literal.into_inner())
6860}
6861
6862fn suffix_length_unit(suffix: NumericSuffix) -> Option<UnitLength> {
6863    match suffix {
6864        NumericSuffix::Mm => Some(UnitLength::Millimeters),
6865        NumericSuffix::Cm => Some(UnitLength::Centimeters),
6866        NumericSuffix::M => Some(UnitLength::Meters),
6867        NumericSuffix::Inch => Some(UnitLength::Inches),
6868        NumericSuffix::Ft => Some(UnitLength::Feet),
6869        NumericSuffix::Yd => Some(UnitLength::Yards),
6870        _ => None,
6871    }
6872}
6873
6874fn number_value_in_default_length_units(number: Number, default_length_unit: UnitLength) -> f64 {
6875    match suffix_length_unit(number.units) {
6876        Some(unit) => adjust_length(unit, number.value, default_length_unit).0,
6877        None => number.value,
6878    }
6879}
6880
6881fn literal_value_in_default_length_units(literal: &ast::NumericLiteral, default_length_unit: UnitLength) -> f64 {
6882    match suffix_length_unit(literal.suffix) {
6883        Some(unit) => adjust_length(unit, literal.value, default_length_unit).0,
6884        None => literal.value,
6885    }
6886}
6887
6888fn var_solution_needs_commit(
6889    current_literal: &ast::NumericLiteral,
6890    solved_value: Number,
6891    default_length_unit: UnitLength,
6892) -> bool {
6893    let current = literal_value_in_default_length_units(current_literal, default_length_unit);
6894    let solved = number_value_in_default_length_units(solved_value, default_length_unit);
6895
6896    (current - solved).abs() > 1e-9
6897}
6898
6899fn preserve_var_solution_literal_style(
6900    current_literal: &ast::NumericLiteral,
6901    solved_value: Number,
6902    default_length_unit: UnitLength,
6903) -> Number {
6904    if current_literal.suffix == NumericSuffix::None {
6905        return Number {
6906            value: number_value_in_default_length_units(solved_value, default_length_unit),
6907            units: NumericSuffix::None,
6908        };
6909    }
6910
6911    let Some(current_unit) = suffix_length_unit(current_literal.suffix) else {
6912        return solved_value;
6913    };
6914
6915    let solved_default_value = number_value_in_default_length_units(solved_value, default_length_unit);
6916    Number {
6917        value: adjust_length(default_length_unit, solved_default_value, current_unit).0,
6918        units: current_literal.suffix,
6919    }
6920}
6921
6922pub(crate) fn to_ast_point2d(point: &Point2d<Expr>) -> anyhow::Result<ast::Expr> {
6923    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node {
6924        inner: ast::ArrayExpression {
6925            elements: vec![to_source_expr(&point.x)?, to_source_expr(&point.y)?],
6926            non_code_meta: Default::default(),
6927            digest: None,
6928        },
6929        start: Default::default(),
6930        end: Default::default(),
6931        module_id: Default::default(),
6932        node_path: None,
6933        outer_attrs: Default::default(),
6934        pre_comments: Default::default(),
6935        comment_start: Default::default(),
6936    })))
6937}
6938
6939pub(crate) fn to_ast_point2d_array(points: &[Point2d<Expr>]) -> anyhow::Result<ast::Expr> {
6940    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6941        ast::ArrayExpression {
6942            elements: points.iter().map(to_ast_point2d).collect::<anyhow::Result<Vec<_>>>()?,
6943            digest: None,
6944            non_code_meta: Default::default(),
6945        },
6946    ))))
6947}
6948
6949fn to_ast_point2d_number(point: &Point2d<Number>) -> anyhow::Result<ast::Expr> {
6950    Ok(ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(
6951        ast::ArrayExpression {
6952            elements: vec![
6953                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6954                    point.x,
6955                )?)))),
6956                ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
6957                    point.y,
6958                )?)))),
6959            ],
6960            non_code_meta: Default::default(),
6961            digest: None,
6962        },
6963    ))))
6964}
6965
6966fn to_source_expr(expr: &Expr) -> anyhow::Result<ast::Expr> {
6967    match expr {
6968        Expr::Number(number) => Ok(ast::Expr::Literal(BoxNode::new(ast::Node {
6969            inner: ast::Literal::from(to_source_number(*number)?),
6970            start: Default::default(),
6971            end: Default::default(),
6972            module_id: Default::default(),
6973            node_path: None,
6974            outer_attrs: Default::default(),
6975            pre_comments: Default::default(),
6976            comment_start: Default::default(),
6977        }))),
6978        Expr::Var(number) => Ok(ast::Expr::SketchVar(BoxNode::new(ast::Node {
6979            inner: ast::SketchVar {
6980                initial: Some(BoxNode::new(ast::Node {
6981                    inner: to_source_number(*number)?,
6982                    start: Default::default(),
6983                    end: Default::default(),
6984                    module_id: Default::default(),
6985                    node_path: None,
6986                    outer_attrs: Default::default(),
6987                    pre_comments: Default::default(),
6988                    comment_start: Default::default(),
6989                })),
6990                digest: None,
6991            },
6992            start: Default::default(),
6993            end: Default::default(),
6994            module_id: Default::default(),
6995            node_path: None,
6996            outer_attrs: Default::default(),
6997            pre_comments: Default::default(),
6998            comment_start: Default::default(),
6999        }))),
7000        Expr::Variable(variable) => Ok(ast_name_expr(variable.clone())),
7001    }
7002}
7003
7004fn to_source_number(number: Number) -> anyhow::Result<ast::NumericLiteral> {
7005    Ok(ast::NumericLiteral {
7006        value: number.value,
7007        suffix: number.units,
7008        raw: format_number_literal(number.value, number.units, None)?,
7009        digest: None,
7010    })
7011}
7012
7013pub(crate) fn ast_name_expr(name: String) -> ast::Expr {
7014    ast::Expr::Name(BoxNode::new(ast_name(name)))
7015}
7016
7017fn ast_name(name: String) -> ast::Node<ast::Name> {
7018    ast::Node {
7019        inner: ast::Name {
7020            name: ast::Node {
7021                inner: ast::Identifier { name, digest: None },
7022                start: Default::default(),
7023                end: Default::default(),
7024                module_id: Default::default(),
7025                node_path: None,
7026                outer_attrs: Default::default(),
7027                pre_comments: Default::default(),
7028                comment_start: Default::default(),
7029            },
7030            path: Vec::new(),
7031            abs_path: false,
7032            digest: None,
7033        },
7034        start: Default::default(),
7035        end: Default::default(),
7036        module_id: Default::default(),
7037        node_path: None,
7038        outer_attrs: Default::default(),
7039        pre_comments: Default::default(),
7040        comment_start: Default::default(),
7041    }
7042}
7043
7044pub(crate) fn ast_sketch2_name(name: &str) -> ast::Name {
7045    ast::Name {
7046        name: ast::Node {
7047            inner: ast::Identifier {
7048                name: name.to_owned(),
7049                digest: None,
7050            },
7051            start: Default::default(),
7052            end: Default::default(),
7053            module_id: Default::default(),
7054            node_path: None,
7055            outer_attrs: Default::default(),
7056            pre_comments: Default::default(),
7057            comment_start: Default::default(),
7058        },
7059        path: Default::default(),
7060        abs_path: false,
7061        digest: None,
7062    }
7063}
7064
7065/// Create an AST node for coincident([expr1, expr2, ...])
7066pub(crate) fn create_coincident_ast(exprs: impl IntoIterator<Item = ast::Expr>) -> ast::Expr {
7067    let elements = exprs.into_iter().collect::<Vec<_>>();
7068    debug_assert!(elements.len() >= 2, "Coincident AST should have at least 2 inputs");
7069
7070    // Create array [expr1, expr2, ...]
7071    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7072        elements,
7073        digest: None,
7074        non_code_meta: Default::default(),
7075    })));
7076
7077    // Create coincident([...])
7078    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7079        callee: ast::Node::no_src(ast_sketch2_name(COINCIDENT_FN)),
7080        unlabeled: Some(array_expr),
7081        arguments: Default::default(),
7082        digest: None,
7083        non_code_meta: Default::default(),
7084    })))
7085}
7086
7087/// Create an AST node for horizontal(line)
7088pub(crate) fn create_horizontal_ast(line_expr: ast::Expr) -> ast::Expr {
7089    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7090        callee: ast::Node::no_src(ast_sketch2_name(HORIZONTAL_FN)),
7091        unlabeled: Some(line_expr),
7092        arguments: Default::default(),
7093        digest: None,
7094        non_code_meta: Default::default(),
7095    })))
7096}
7097
7098/// Create an AST node for vertical(line)
7099pub(crate) fn create_vertical_ast(line_expr: ast::Expr) -> ast::Expr {
7100    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7101        callee: ast::Node::no_src(ast_sketch2_name(VERTICAL_FN)),
7102        unlabeled: Some(line_expr),
7103        arguments: Default::default(),
7104        digest: None,
7105        non_code_meta: Default::default(),
7106    })))
7107}
7108
7109/// Create a member expression like object.property (e.g., line1.end)
7110pub(crate) fn create_member_expression(object_expr: ast::Expr, property: &str) -> ast::Expr {
7111    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7112        object: object_expr,
7113        property: ast::Expr::Name(BoxNode::new(ast::Node::no_src(ast::Name {
7114            name: ast::Node::no_src(ast::Identifier {
7115                name: property.to_string(),
7116                digest: None,
7117            }),
7118            path: Vec::new(),
7119            abs_path: false,
7120            digest: None,
7121        }))),
7122        computed: false,
7123        digest: None,
7124    })))
7125}
7126
7127pub(crate) fn create_index_expression(object_expr: ast::Expr, index: usize) -> ast::Expr {
7128    ast::Expr::MemberExpression(BoxNode::new(ast::Node::no_src(ast::MemberExpression {
7129        object: object_expr,
7130        property: ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(
7131            ast::NumericLiteral {
7132                value: index as f64,
7133                suffix: NumericSuffix::None,
7134                raw: index.to_string(),
7135                digest: None,
7136            },
7137        )))),
7138        computed: true,
7139        digest: None,
7140    })))
7141}
7142
7143/// Create an AST node for `fixed([point, [x, y]])`.
7144fn create_fixed_point_constraint_ast(point_expr: ast::Expr, position: Point2d<Number>) -> anyhow::Result<ast::Expr> {
7145    // Create [x, y] array literal.
7146    let x_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7147        position.x,
7148    )?))));
7149    let y_literal = ast::Expr::Literal(BoxNode::new(ast::Node::no_src(ast::Literal::from(to_source_number(
7150        position.y,
7151    )?))));
7152    let point_array = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7153        elements: vec![x_literal, y_literal],
7154        digest: None,
7155        non_code_meta: Default::default(),
7156    })));
7157
7158    // Create [point, [x, y]] outer array.
7159    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7160        elements: vec![point_expr, point_array],
7161        digest: None,
7162        non_code_meta: Default::default(),
7163    })));
7164
7165    // Create fixed([...])
7166    Ok(ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(
7167        ast::CallExpressionKw {
7168            callee: ast::Node::no_src(ast_sketch2_name(FIXED_FN)),
7169            unlabeled: Some(array_expr),
7170            arguments: Default::default(),
7171            digest: None,
7172            non_code_meta: Default::default(),
7173        },
7174    ))))
7175}
7176
7177/// Create an AST node for equalLength([line1, line2, ...])
7178pub(crate) fn create_equal_length_ast(line_exprs: Vec<ast::Expr>) -> ast::Expr {
7179    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7180        elements: line_exprs,
7181        digest: None,
7182        non_code_meta: Default::default(),
7183    })));
7184
7185    // Create equalLength([...])
7186    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7187        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_LENGTH_FN)),
7188        unlabeled: Some(array_expr),
7189        arguments: Default::default(),
7190        digest: None,
7191        non_code_meta: Default::default(),
7192    })))
7193}
7194
7195/// Create an AST node for equalRadius([seg1, seg2, ...])
7196pub(crate) fn create_equal_radius_ast(segment_exprs: Vec<ast::Expr>) -> ast::Expr {
7197    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7198        elements: segment_exprs,
7199        digest: None,
7200        non_code_meta: Default::default(),
7201    })));
7202
7203    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7204        callee: ast::Node::no_src(ast_sketch2_name(EQUAL_RADIUS_FN)),
7205        unlabeled: Some(array_expr),
7206        arguments: Default::default(),
7207        digest: None,
7208        non_code_meta: Default::default(),
7209    })))
7210}
7211
7212/// Create an AST node for tangent([seg1, seg2])
7213pub(crate) fn create_tangent_ast(seg1_expr: ast::Expr, seg2_expr: ast::Expr) -> ast::Expr {
7214    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7215        elements: vec![seg1_expr, seg2_expr],
7216        digest: None,
7217        non_code_meta: Default::default(),
7218    })));
7219
7220    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7221        callee: ast::Node::no_src(ast_sketch2_name(TANGENT_FN)),
7222        unlabeled: Some(array_expr),
7223        arguments: Default::default(),
7224        digest: None,
7225        non_code_meta: Default::default(),
7226    })))
7227}
7228
7229/// Create an AST node for symmetric([input1, input2], axis = line)
7230pub(crate) fn create_symmetric_ast(input_exprs: Vec<ast::Expr>, axis_expr: ast::Expr) -> ast::Expr {
7231    let array_expr = ast::Expr::ArrayExpression(BoxNode::new(ast::Node::no_src(ast::ArrayExpression {
7232        elements: input_exprs,
7233        digest: None,
7234        non_code_meta: Default::default(),
7235    })));
7236    let arguments = vec![ast::LabeledArg {
7237        label: Some(ast::Identifier::new(SYMMETRIC_AXIS_PARAM)),
7238        arg: axis_expr,
7239    }];
7240
7241    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7242        callee: ast::Node::no_src(ast_sketch2_name(SYMMETRIC_FN)),
7243        unlabeled: Some(array_expr),
7244        arguments,
7245        digest: None,
7246        non_code_meta: Default::default(),
7247    })))
7248}
7249
7250/// Create an AST node for midpoint(segment, point = point)
7251pub(crate) fn create_midpoint_ast(segment_expr: ast::Expr, point_expr: ast::Expr) -> ast::Expr {
7252    let arguments = vec![ast::LabeledArg {
7253        label: Some(ast::Identifier::new(MIDPOINT_POINT_PARAM)),
7254        arg: point_expr,
7255    }];
7256
7257    ast::Expr::CallExpressionKw(BoxNode::new(ast::Node::no_src(ast::CallExpressionKw {
7258        callee: ast::Node::no_src(ast_sketch2_name(MIDPOINT_FN)),
7259        unlabeled: Some(segment_expr),
7260        arguments,
7261        digest: None,
7262        non_code_meta: Default::default(),
7263    })))
7264}
7265
7266/// The source range to attach to a diagnostic for `error` in the top-level
7267/// module. Source ranges are ordered innermost first and may point into
7268/// imported modules, so prefer the innermost range that is in the top-level
7269/// module; otherwise fall back to the innermost range.
7270fn issue_source_range(error: &KclError) -> SourceRange {
7271    let source_ranges = error.source_ranges();
7272    source_ranges
7273        .iter()
7274        .find(|range| range.is_top_level_module())
7275        .or_else(|| source_ranges.first())
7276        .copied()
7277        .unwrap_or_else(SourceRange::synthetic)
7278}
7279
7280#[cfg(test)]
7281mod tests {
7282    use std::sync;
7283
7284    use kcl_api::Distance;
7285    use kcl_api::Fixed;
7286    use kcl_api::FixedPoint;
7287    use kcl_api::Midpoint;
7288    use kcl_api::NumericSuffix;
7289    use kcl_api::Object;
7290    use kcl_api::Plane;
7291    use kcl_api::Sketch;
7292    use kcl_api::Tangent;
7293    use kcl_api::Vertical;
7294
7295    use super::*;
7296    use crate::engine::PlaneName;
7297    use crate::engine::engine_manager::EngineManager;
7298    use crate::execution::cache::SketchModeState;
7299    use crate::execution::cache::clear_mem_cache;
7300    use crate::execution::cache::read_old_memory;
7301    use crate::execution::cache::write_old_memory;
7302
7303    fn find_first_sketch_object(scene_graph: &SceneGraph) -> Option<&Object> {
7304        for object in &scene_graph.objects {
7305            if let ObjectKind::Sketch(_) = &object.kind {
7306                return Some(object);
7307            }
7308        }
7309        None
7310    }
7311
7312    fn find_first_face_object(scene_graph: &SceneGraph) -> Option<&Object> {
7313        for object in &scene_graph.objects {
7314            if let ObjectKind::Face(_) = &object.kind {
7315                return Some(object);
7316            }
7317        }
7318        None
7319    }
7320
7321    fn find_first_wall_object_id(scene_graph: &SceneGraph) -> Option<ObjectId> {
7322        for object in &scene_graph.objects {
7323            if matches!(&object.kind, ObjectKind::Wall(_)) {
7324                return Some(object.id);
7325            }
7326        }
7327        None
7328    }
7329
7330    fn find_cap_object_id_with_solid_output_index(
7331        scene_graph: &SceneGraph,
7332        cap_kind: kcl_api::CapKind,
7333        solid_output_index: usize,
7334    ) -> Option<ObjectId> {
7335        for object in &scene_graph.objects {
7336            if matches!(&object.kind, ObjectKind::Cap(cap) if cap.kind == cap_kind && cap.solid_output_index == Some(solid_output_index))
7337            {
7338                return Some(object.id);
7339            }
7340        }
7341        None
7342    }
7343
7344    #[test]
7345    fn issue_source_range_prefers_top_level_module() {
7346        use kcl_error::ModuleId;
7347
7348        let top = SourceRange::new(10, 20, ModuleId::default());
7349        let imported = SourceRange::new(0, 5, ModuleId::from_usize(7));
7350
7351        // Innermost frame is in an imported module; the diagnostic should
7352        // land on the innermost top-level range instead.
7353        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new(
7354            "boom".to_owned(),
7355            vec![imported, top],
7356        ));
7357        assert_eq!(super::issue_source_range(&error), top);
7358
7359        // No top-level range at all: fall back to the innermost one.
7360        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![imported]));
7361        assert_eq!(super::issue_source_range(&error), imported);
7362
7363        // No ranges: synthetic.
7364        let error = KclError::new_semantic(crate::errors::KclErrorDetails::new("boom".to_owned(), vec![]));
7365        assert_eq!(super::issue_source_range(&error), SourceRange::synthetic());
7366    }
7367
7368    #[test]
7369    fn composite_constituent_sweeps_are_not_solid_outputs() {
7370        use kcl_api::artifact::ArtifactSweepMethod;
7371        use kcl_api::artifact::CompositeSolid;
7372        use kcl_api::artifact::CompositeSolidSubType;
7373        use kcl_api::artifact::Sweep;
7374        use kcl_api::artifact::SweepSubType;
7375
7376        let first_sweep_id = ArtifactId::new(Uuid::new_v4());
7377        let second_sweep_id = ArtifactId::new(Uuid::new_v4());
7378        let composite_id = ArtifactId::new(Uuid::new_v4());
7379        let code_ref = CodeRef::placeholder(SourceRange::synthetic());
7380        let sweep = |id| {
7381            Artifact::Sweep(Sweep {
7382                id,
7383                sub_type: SweepSubType::Extrusion,
7384                path_id: Some(ArtifactId::new(Uuid::new_v4())),
7385                surface_ids: Vec::new(),
7386                edge_ids: Vec::new(),
7387                code_ref: code_ref.clone(),
7388                source_sweep_id: None,
7389                trajectory_id: None,
7390                method: ArtifactSweepMethod::New,
7391                consumed: false,
7392                pattern_ids: Vec::new(),
7393            })
7394        };
7395        let mut artifacts = IndexMap::from([
7396            (first_sweep_id, sweep(first_sweep_id)),
7397            (second_sweep_id, sweep(second_sweep_id)),
7398        ]);
7399
7400        let top_level_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7401        assert_eq!(
7402            solid_output_index_for_sweep(&top_level_graph, first_sweep_id, &code_ref),
7403            Some(0)
7404        );
7405        assert_eq!(
7406            solid_output_index_for_sweep(&top_level_graph, second_sweep_id, &code_ref),
7407            Some(1)
7408        );
7409
7410        artifacts.insert(
7411            composite_id,
7412            Artifact::CompositeSolid(CompositeSolid {
7413                id: composite_id,
7414                consumed: false,
7415                sub_type: CompositeSolidSubType::Union,
7416                output_index: None,
7417                solid_ids: vec![first_sweep_id, second_sweep_id],
7418                tool_ids: Vec::new(),
7419                code_ref,
7420                composite_solid_id: None,
7421                pattern_ids: Vec::new(),
7422            }),
7423        );
7424        let composite_graph = ArtifactGraph::from_parts(artifacts.clone(), artifacts.len());
7425        assert_eq!(
7426            solid_output_index_for_sweep(&composite_graph, first_sweep_id, &CodeRef::default()),
7427            None
7428        );
7429        assert_eq!(
7430            solid_output_index_for_sweep(&composite_graph, second_sweep_id, &CodeRef::default()),
7431            None
7432        );
7433    }
7434
7435    #[test]
7436    fn test_region_name_from_sweep_variable_supports_sweep_kinds() {
7437        let source = "\
7438region001 = region(point = [0.1, 0.1], sketch = s)
7439extrude001 = extrude(region001, length = 5)
7440revolve001 = revolve(region001, axis = Y)
7441sweep001 = sweep(region001, path = path001)
7442loft001 = loft(region001)
7443not_sweep001 = shell(extrude001, faces = [], thickness = 1)
7444";
7445
7446        let program = Program::parse(source).unwrap().0.unwrap();
7447
7448        assert_eq!(
7449            region_name_from_sweep_variable(&program.ast, "extrude001"),
7450            Some("region001".to_owned())
7451        );
7452        assert_eq!(
7453            region_name_from_sweep_variable(&program.ast, "revolve001"),
7454            Some("region001".to_owned())
7455        );
7456        assert_eq!(
7457            region_name_from_sweep_variable(&program.ast, "sweep001"),
7458            Some("region001".to_owned())
7459        );
7460        assert_eq!(
7461            region_name_from_sweep_variable(&program.ast, "loft001"),
7462            Some("region001".to_owned())
7463        );
7464        assert_eq!(region_name_from_sweep_variable(&program.ast, "not_sweep001"), None);
7465    }
7466
7467    #[track_caller]
7468    fn expect_sketch(object: &Object) -> &Sketch {
7469        if let ObjectKind::Sketch(sketch) = &object.kind {
7470            sketch
7471        } else {
7472            panic!("Object is not a sketch: {:?}", object);
7473        }
7474    }
7475
7476    fn point_position(scene_graph: &SceneGraph, point_id: ObjectId) -> Point2d<Number> {
7477        let point_object = scene_graph.objects.get(point_id.0).unwrap();
7478        let ObjectKind::Segment {
7479            segment: Segment::Point(point),
7480        } = &point_object.kind
7481        else {
7482            panic!("Object is not a point segment: {point_object:?}");
7483        };
7484        point.position.clone()
7485    }
7486
7487    fn assert_point_position_close(actual: Point2d<Number>, expected: Point2d<Number>) {
7488        assert!((actual.x.value - expected.x.value).abs() < 1e-6);
7489        assert!((actual.y.value - expected.y.value).abs() < 1e-6);
7490    }
7491
7492    /// Build a millimeter-valued point expression for concise sketch edit test
7493    /// setup.
7494    fn point_expr_mm(x: f64, y: f64) -> Point2d<Expr> {
7495        Point2d {
7496            x: Expr::Var(Number {
7497                value: x,
7498                units: NumericSuffix::Mm,
7499            }),
7500            y: Expr::Var(Number {
7501                value: y,
7502                units: NumericSuffix::Mm,
7503            }),
7504        }
7505    }
7506
7507    /// Build a millimeter-valued numeric point for comparing solved scene graph
7508    /// positions.
7509    fn point_number_mm(x: f64, y: f64) -> Point2d<Number> {
7510        Point2d {
7511            x: Number {
7512                value: x,
7513                units: NumericSuffix::Mm,
7514            },
7515            y: Number {
7516                value: y,
7517                units: NumericSuffix::Mm,
7518            },
7519        }
7520    }
7521
7522    fn make_line_ctor(start_x: f64, start_y: f64, end_x: f64, end_y: f64, units: NumericSuffix) -> LineCtor {
7523        LineCtor {
7524            start: Point2d {
7525                x: Expr::Number(Number { value: start_x, units }),
7526                y: Expr::Number(Number { value: start_y, units }),
7527            },
7528            end: Point2d {
7529                x: Expr::Number(Number { value: end_x, units }),
7530                y: Expr::Number(Number { value: end_y, units }),
7531            },
7532            construction: None,
7533        }
7534    }
7535
7536    async fn create_sketch_with_single_line(
7537        frontend: &mut FrontendState,
7538        ctx: &ExecutorContext,
7539        mock_ctx: &ExecutorContext,
7540        version: Version,
7541    ) -> (ObjectId, ObjectId, SourceDelta, SceneGraphDelta) {
7542        frontend.program = Program::empty();
7543
7544        let sketch_args = SketchCtor {
7545            on: Plane::Default(PlaneName::Xy),
7546        };
7547        let (_src_delta, _scene_delta, sketch_id) = frontend
7548            .new_sketch(ctx, ProjectId(0), FileId(0), version, sketch_args)
7549            .await
7550            .unwrap();
7551
7552        let segment = SegmentCtor::Line(make_line_ctor(0.0, 0.0, 10.0, 10.0, NumericSuffix::Mm));
7553        let (source_delta, scene_graph_delta) = frontend
7554            .add_segment(mock_ctx, version, sketch_id, segment, None)
7555            .await
7556            .unwrap();
7557        let line_id = *scene_graph_delta
7558            .new_objects
7559            .last()
7560            .expect("Expected line object id to be created");
7561
7562        (sketch_id, line_id, source_delta, scene_graph_delta)
7563    }
7564
7565    async fn seed_frontend_with_mock(frontend: &mut FrontendState, mock_ctx: &ExecutorContext, program: &Program) {
7566        frontend.program = program.clone();
7567        let outcome = mock_ctx.run_mock(program, &MockConfig::default()).await.unwrap();
7568        frontend.update_state_after_exec(outcome, true);
7569    }
7570
7571    #[test]
7572    fn test_parse_frontend_mutation_source_error_messages_are_user_facing() {
7573        for (source, expected_message) in [
7574            ("**", "Error parsing KCL source after editing: Unexpected token: *"),
7575            (
7576                "3'",
7577                "Error parsing KCL source after editing: unterminated string literal",
7578            ),
7579        ] {
7580            let err = parse_frontend_mutation_source(
7581                source,
7582                "Error parsing KCL source after editing",
7583                "No AST produced after editing",
7584            )
7585            .expect_err("expected invalid KCL source to fail");
7586            let message = err.error.message();
7587
7588            assert_eq!(message, expected_message);
7589            assert!(!message.contains("CompilationIssue"));
7590            assert!(!message.contains("KclErrorDetails"));
7591            assert!(!message.contains("source_range"));
7592        }
7593    }
7594
7595    #[tokio::test(flavor = "multi_thread")]
7596    async fn test_edit_constraint_value_parse_error_messages_are_user_facing() {
7597        let initial_source = "\
7598sketch(on = XY) {
7599  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7600  distance([line1.start, line1.end]) == 10
7601}
7602";
7603        let program = Program::parse(initial_source).unwrap().0.unwrap();
7604
7605        let mut frontend = FrontendState::new();
7606        let mock_ctx = ExecutorContext::new_mock(None).await;
7607        let version = Version(0);
7608
7609        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7610        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7611        let sketch_id = sketch_object.id;
7612        let sketch = expect_sketch(sketch_object);
7613        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7614
7615        for (value, expected_message) in [
7616            ("**", "Invalid constraint value: Unexpected token: *"),
7617            ("3'", "Invalid constraint value: unterminated string literal"),
7618        ] {
7619            let err = frontend
7620                .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, value.to_owned())
7621                .await
7622                .expect_err("expected invalid constraint expression to fail");
7623            let message = err.error.message();
7624
7625            assert_eq!(message, expected_message);
7626            assert!(!message.contains("CompilationIssue"));
7627            assert!(!message.contains("KclErrorDetails"));
7628            assert!(!message.contains("source_range"));
7629        }
7630
7631        mock_ctx.close().await;
7632    }
7633
7634    #[tokio::test(flavor = "multi_thread")]
7635    async fn test_failed_edit_constraint_value_does_not_update_program() {
7636        let initial_source = "\
7637sketch(on = XY) {
7638  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7639  distance([line1.start, line1.end]) == 10
7640}
7641";
7642        let program = Program::parse(initial_source).unwrap().0.unwrap();
7643        let original_source = program.original_file_contents.clone();
7644
7645        let mut frontend = FrontendState::new();
7646        let mock_ctx = ExecutorContext::new_mock(None).await;
7647        let version = Version(0);
7648
7649        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7650        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7651        let sketch_id = sketch_object.id;
7652        let sketch = expect_sketch(sketch_object);
7653        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7654
7655        frontend
7656            .edit_constraint_value(
7657                &mock_ctx,
7658                version,
7659                sketch_id,
7660                constraint_id,
7661                "unknownDistance".to_owned(),
7662            )
7663            .await
7664            .expect_err("expected invalid constraint value to fail execution");
7665
7666        assert_eq!(frontend.program.original_file_contents, original_source);
7667        assert!(!frontend.program.original_file_contents.contains("unknownDistance"));
7668
7669        mock_ctx.close().await;
7670    }
7671
7672    #[tokio::test(flavor = "multi_thread")]
7673    async fn test_edit_constraint_value_array_index_oob_fails_in_sketch_mode() {
7674        let initial_source = "\
7675arr = [0]
7676sketch(on = XY) {
7677  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
7678  distance([line1.start, line1.end]) == 10
7679}
7680";
7681        let program = Program::parse(initial_source).unwrap().0.unwrap();
7682
7683        let mut frontend = FrontendState::new();
7684        let mock_ctx = ExecutorContext::new_mock(None).await;
7685        let version = Version(0);
7686
7687        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
7688        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
7689        let sketch_id = sketch_object.id;
7690        let sketch = expect_sketch(sketch_object);
7691        let constraint_id = *sketch.constraints.first().expect("expected distance constraint");
7692
7693        // In sketch mode execution, an out-of-bounds array index is an error.
7694        // It should not fall back to the first element of the array the way
7695        // plain mock execution does.
7696        let err = frontend
7697            .edit_constraint_value(&mock_ctx, version, sketch_id, constraint_id, "arr[5]".to_owned())
7698            .await
7699            .expect_err("expected out-of-bounds array index to be an error in sketch mode execution");
7700        let message = err.error.message();
7701        assert!(
7702            message.contains("The array doesn't have any item at index 5"),
7703            "unexpected error message: {message}"
7704        );
7705
7706        mock_ctx.close().await;
7707    }
7708
7709    #[tokio::test(flavor = "multi_thread")]
7710    async fn test_sketch_checkpoint_round_trip_restores_state() {
7711        let mut frontend = FrontendState::new();
7712        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7713        let mock_ctx = ExecutorContext::new_mock(None).await;
7714        let version = Version(0);
7715
7716        let (sketch_id, line_id, source_delta, scene_graph_delta) =
7717            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7718
7719        let expected_source = source_delta.text.clone();
7720        let expected_scene_graph = frontend.scene_graph.clone();
7721        let expected_exec_outcome = scene_graph_delta.exec_outcome.clone();
7722        let expected_point_freedom_cache = frontend.point_freedom_cache.clone();
7723
7724        let checkpoint_id = frontend
7725            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7726            .await
7727            .unwrap();
7728
7729        let edited_segments = vec![ExistingSegmentCtor {
7730            id: line_id,
7731            ctor: SegmentCtor::Line(make_line_ctor(1.0, 2.0, 13.0, 14.0, NumericSuffix::Mm)),
7732        }];
7733        let (edited_source, _edited_scene) = frontend
7734            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
7735            .await
7736            .unwrap();
7737        assert_ne!(edited_source.text, expected_source);
7738
7739        let restored = frontend.restore_sketch_checkpoint(checkpoint_id).await.unwrap();
7740
7741        assert_eq!(restored.source_delta.text, expected_source);
7742        assert_eq!(restored.scene_graph_delta.new_graph, expected_scene_graph);
7743        assert!(restored.scene_graph_delta.invalidates_ids);
7744        assert_eq!(restored.scene_graph_delta.exec_outcome, expected_exec_outcome);
7745        assert_eq!(frontend.scene_graph, expected_scene_graph);
7746        assert_eq!(frontend.point_freedom_cache, expected_point_freedom_cache);
7747
7748        ctx.close().await;
7749    }
7750
7751    #[tokio::test(flavor = "multi_thread")]
7752    async fn test_sketch_checkpoints_prune_oldest_entries() {
7753        let mut frontend = FrontendState::new();
7754        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7755        let mock_ctx = ExecutorContext::new_mock(None).await;
7756        let version = Version(0);
7757
7758        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7759            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7760
7761        let mut checkpoint_ids = Vec::new();
7762        for _ in 0..(MAX_SKETCH_CHECKPOINTS + 3) {
7763            checkpoint_ids.push(
7764                frontend
7765                    .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7766                    .await
7767                    .unwrap(),
7768            );
7769        }
7770
7771        assert_eq!(frontend.sketch_checkpoints.len(), MAX_SKETCH_CHECKPOINTS);
7772        assert!(checkpoint_ids.windows(2).all(|ids| ids[0] < ids[1]));
7773
7774        let oldest_retained = checkpoint_ids[3];
7775        assert_eq!(
7776            frontend.sketch_checkpoints.front().map(|checkpoint| checkpoint.id),
7777            Some(oldest_retained)
7778        );
7779
7780        let evicted_restore = frontend.restore_sketch_checkpoint(checkpoint_ids[0]).await;
7781        assert!(evicted_restore.is_err());
7782        assert!(evicted_restore.unwrap_err().msg.contains("Sketch checkpoint not found"));
7783
7784        frontend
7785            .restore_sketch_checkpoint(*checkpoint_ids.last().unwrap())
7786            .await
7787            .unwrap();
7788
7789        ctx.close().await;
7790    }
7791
7792    #[tokio::test(flavor = "multi_thread")]
7793    async fn test_restore_sketch_checkpoint_missing_id_returns_error() {
7794        let mut frontend = FrontendState::new();
7795        let missing_checkpoint = SketchCheckpointId::new(999);
7796
7797        let err = frontend
7798            .restore_sketch_checkpoint(missing_checkpoint)
7799            .await
7800            .expect_err("Expected restore to fail for missing checkpoint");
7801
7802        assert!(err.msg.contains("Sketch checkpoint not found"));
7803    }
7804
7805    #[tokio::test(flavor = "multi_thread")]
7806    async fn test_clear_sketch_checkpoints_removes_all_restore_points() {
7807        let mut frontend = FrontendState::new();
7808        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7809        let mock_ctx = ExecutorContext::new_mock(None).await;
7810        let version = Version(0);
7811
7812        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7813            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7814
7815        let checkpoint_a = frontend
7816            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7817            .await
7818            .unwrap();
7819        let checkpoint_b = frontend
7820            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7821            .await
7822            .unwrap();
7823        assert_eq!(frontend.sketch_checkpoints.len(), 2);
7824
7825        frontend.clear_sketch_checkpoints();
7826        assert!(frontend.sketch_checkpoints.is_empty());
7827        frontend.restore_sketch_checkpoint(checkpoint_a).await.unwrap_err();
7828        frontend.restore_sketch_checkpoint(checkpoint_b).await.unwrap_err();
7829
7830        ctx.close().await;
7831    }
7832
7833    #[tokio::test(flavor = "multi_thread")]
7834    async fn test_hack_set_program_keeps_old_checkpoints_and_adds_fresh_baseline() {
7835        let mut frontend = FrontendState::new();
7836        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7837        let mock_ctx = ExecutorContext::new_mock(None).await;
7838        let version = Version(0);
7839
7840        let (_sketch_id, _line_id, source_delta, scene_graph_delta) =
7841            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7842        let old_source = source_delta.text.clone();
7843        let old_checkpoint = frontend
7844            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7845            .await
7846            .unwrap();
7847        let initial_checkpoint_count = frontend.sketch_checkpoints.len();
7848
7849        let new_program = Program::parse("sketch(on = XY) {\n  point(at = [1mm, 2mm])\n}\n")
7850            .unwrap()
7851            .0
7852            .unwrap();
7853
7854        let result = frontend.hack_set_program(&ctx, new_program).await.unwrap();
7855        let SetProgramOutcome::Success {
7856            checkpoint_id: Some(new_checkpoint),
7857            ..
7858        } = result
7859        else {
7860            panic!("Expected Success with a fresh checkpoint baseline");
7861        };
7862
7863        assert_eq!(frontend.sketch_checkpoints.len(), initial_checkpoint_count + 1);
7864
7865        let old_restore = frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7866        assert_eq!(old_restore.source_delta.text, old_source);
7867
7868        let new_restore = frontend.restore_sketch_checkpoint(new_checkpoint).await.unwrap();
7869        assert!(new_restore.source_delta.text.contains("point(at = [1mm, 2mm])"));
7870
7871        ctx.close().await;
7872    }
7873
7874    #[tokio::test(flavor = "multi_thread")]
7875    async fn test_hack_set_program_exec_failure_does_not_add_checkpoint() {
7876        let mut frontend = FrontendState::new();
7877        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7878        let mock_ctx = ExecutorContext::new_mock(None).await;
7879        let version = Version(0);
7880
7881        let (_sketch_id, _line_id, _source_delta, scene_graph_delta) =
7882            create_sketch_with_single_line(&mut frontend, &ctx, &mock_ctx, version).await;
7883        let old_checkpoint = frontend
7884            .create_sketch_checkpoint(scene_graph_delta.exec_outcome.clone())
7885            .await
7886            .unwrap();
7887        let checkpoint_count_before = frontend.sketch_checkpoints.len();
7888
7889        let failing_program = Program::parse(
7890            "sketch(on = XY) {\n  line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])\n}\n\nbad = missing_name\n",
7891        )
7892        .unwrap()
7893        .0
7894        .unwrap();
7895
7896        let result = frontend.hack_set_program(&ctx, failing_program).await.unwrap();
7897        assert!(matches!(result, SetProgramOutcome::ExecFailure { .. }));
7898        assert_eq!(frontend.sketch_checkpoints.len(), checkpoint_count_before);
7899        frontend.restore_sketch_checkpoint(old_checkpoint).await.unwrap();
7900
7901        ctx.close().await;
7902    }
7903
7904    #[tokio::test(flavor = "multi_thread")]
7905    async fn test_restore_sketch_checkpoint_restores_and_clears_mock_memory() {
7906        let mut frontend = FrontendState::new();
7907        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7908
7909        let program = Program::parse(
7910            "width = 2mm\nsketch001 = sketch(on = offsetPlane(XY, offset = width)) {\n  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])\n  distance([line1.start, line1.end]) == width\n}\n",
7911        )
7912        .unwrap()
7913        .0
7914        .unwrap();
7915        let set_program_outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
7916        let SetProgramOutcome::Success { exec_outcome, .. } = set_program_outcome else {
7917            panic!("Expected successful baseline program execution");
7918        };
7919
7920        clear_mem_cache().await;
7921        assert!(read_old_memory().await.is_none());
7922
7923        let checkpoint_without_mock_memory = frontend
7924            .create_sketch_checkpoint((*exec_outcome).clone())
7925            .await
7926            .unwrap();
7927
7928        write_old_memory(SketchModeState::new_for_tests()).await;
7929        assert!(read_old_memory().await.is_some());
7930
7931        let checkpoint_with_mock_memory = frontend
7932            .create_sketch_checkpoint((*exec_outcome).clone())
7933            .await
7934            .unwrap();
7935
7936        clear_mem_cache().await;
7937        assert!(read_old_memory().await.is_none());
7938
7939        frontend
7940            .restore_sketch_checkpoint(checkpoint_with_mock_memory)
7941            .await
7942            .unwrap();
7943        assert!(read_old_memory().await.is_some());
7944
7945        frontend
7946            .restore_sketch_checkpoint(checkpoint_without_mock_memory)
7947            .await
7948            .unwrap();
7949        assert!(read_old_memory().await.is_none());
7950
7951        ctx.close().await;
7952    }
7953
7954    #[tokio::test(flavor = "multi_thread")]
7955    async fn test_hack_set_program_exec_error_still_allows_edit_sketch() {
7956        let source = "\
7957sketch(on = XY) {
7958  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7959}
7960
7961bad = missing_name
7962";
7963        let program = Program::parse(source).unwrap().0.unwrap();
7964
7965        let mut frontend = FrontendState::new();
7966
7967        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
7968        let mock_ctx = ExecutorContext::new_mock(None).await;
7969        let version = Version(0);
7970        let project_id = ProjectId(0);
7971        let file_id = FileId(0);
7972
7973        let SetProgramOutcome::ExecFailure { .. } = frontend.hack_set_program(&ctx, program).await.unwrap() else {
7974            panic!("Expected ExecFailure from hack_set_program due to syntax error in program");
7975        };
7976
7977        let sketch_id = frontend
7978            .scene_graph
7979            .objects
7980            .iter()
7981            .find_map(|obj| matches!(obj.kind, ObjectKind::Sketch(_)).then_some(obj.id))
7982            .expect("Expected sketch object from errored hack_set_program");
7983
7984        frontend
7985            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
7986            .await
7987            .unwrap();
7988
7989        ctx.close().await;
7990        mock_ctx.close().await;
7991    }
7992
7993    #[tokio::test(flavor = "multi_thread")]
7994    async fn test_new_sketch_add_point_edit_point() {
7995        let program = Program::empty();
7996
7997        let mut frontend = FrontendState::new();
7998        frontend.program = program;
7999
8000        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8001        let mock_ctx = ExecutorContext::new_mock(None).await;
8002        let version = Version(0);
8003
8004        let sketch_args = SketchCtor {
8005            on: Plane::Default(PlaneName::Xy),
8006        };
8007        let (_src_delta, scene_delta, sketch_id) = frontend
8008            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8009            .await
8010            .unwrap();
8011        assert_eq!(sketch_id, ObjectId(1));
8012        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8013        let sketch_object = &scene_delta.new_graph.objects[1];
8014        assert_eq!(sketch_object.id, ObjectId(1));
8015        assert_eq!(
8016            sketch_object.kind,
8017            ObjectKind::Sketch(Sketch {
8018                args: SketchCtor {
8019                    on: Plane::Default(PlaneName::Xy)
8020                },
8021                plane: ObjectId(0),
8022                segments: vec![],
8023                constraints: vec![],
8024            })
8025        );
8026        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8027
8028        let point_ctor = PointCtor {
8029            position: Point2d {
8030                x: Expr::Number(Number {
8031                    value: 1.0,
8032                    units: NumericSuffix::Inch,
8033                }),
8034                y: Expr::Number(Number {
8035                    value: 2.0,
8036                    units: NumericSuffix::Inch,
8037                }),
8038            },
8039        };
8040        let segment = SegmentCtor::Point(point_ctor);
8041        let (src_delta, scene_delta) = frontend
8042            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8043            .await
8044            .unwrap();
8045        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_1", src_delta.text.as_str());
8046        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
8047        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8048        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8049            assert_eq!(scene_object.id.0, i);
8050        }
8051
8052        let point_id = *scene_delta.new_objects.last().unwrap();
8053
8054        let point_ctor = PointCtor {
8055            position: Point2d {
8056                x: Expr::Number(Number {
8057                    value: 3.0,
8058                    units: NumericSuffix::Inch,
8059                }),
8060                y: Expr::Number(Number {
8061                    value: 4.0,
8062                    units: NumericSuffix::Inch,
8063                }),
8064            },
8065        };
8066        let segments = vec![ExistingSegmentCtor {
8067            id: point_id,
8068            ctor: SegmentCtor::Point(point_ctor),
8069        }];
8070        let (src_delta, scene_delta) = frontend
8071            .edit_segments(&mock_ctx, version, sketch_id, segments)
8072            .await
8073            .unwrap();
8074        insta::assert_snapshot!("test_new_sketch_add_point_edit_point_2", src_delta.text.as_str());
8075        assert_eq!(scene_delta.new_objects, vec![]);
8076        assert_eq!(scene_delta.new_graph.objects.len(), 3);
8077
8078        ctx.close().await;
8079        mock_ctx.close().await;
8080    }
8081
8082    #[tokio::test(flavor = "multi_thread")]
8083    async fn test_new_sketch_add_line_edit_line() {
8084        let program = Program::empty();
8085
8086        let mut frontend = FrontendState::new();
8087        frontend.program = program;
8088
8089        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8090        let mock_ctx = ExecutorContext::new_mock(None).await;
8091        let version = Version(0);
8092
8093        let sketch_args = SketchCtor {
8094            on: Plane::Default(PlaneName::Xy),
8095        };
8096        let (_src_delta, scene_delta, sketch_id) = frontend
8097            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8098            .await
8099            .unwrap();
8100        assert_eq!(sketch_id, ObjectId(1));
8101        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8102        let sketch_object = &scene_delta.new_graph.objects[1];
8103        assert_eq!(sketch_object.id, ObjectId(1));
8104        assert_eq!(
8105            sketch_object.kind,
8106            ObjectKind::Sketch(Sketch {
8107                args: SketchCtor {
8108                    on: Plane::Default(PlaneName::Xy)
8109                },
8110                plane: ObjectId(0),
8111                segments: vec![],
8112                constraints: vec![],
8113            })
8114        );
8115        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8116
8117        let line_ctor = LineCtor {
8118            start: Point2d {
8119                x: Expr::Number(Number {
8120                    value: 0.0,
8121                    units: NumericSuffix::Mm,
8122                }),
8123                y: Expr::Number(Number {
8124                    value: 0.0,
8125                    units: NumericSuffix::Mm,
8126                }),
8127            },
8128            end: Point2d {
8129                x: Expr::Number(Number {
8130                    value: 10.0,
8131                    units: NumericSuffix::Mm,
8132                }),
8133                y: Expr::Number(Number {
8134                    value: 10.0,
8135                    units: NumericSuffix::Mm,
8136                }),
8137            },
8138            construction: None,
8139        };
8140        let segment = SegmentCtor::Line(line_ctor);
8141        let (src_delta, scene_delta) = frontend
8142            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8143            .await
8144            .unwrap();
8145        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_1", src_delta.text.as_str());
8146        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8147        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8148        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8149            assert_eq!(scene_object.id.0, i);
8150        }
8151
8152        // The new objects are the end points and then the line.
8153        let line = *scene_delta.new_objects.last().unwrap();
8154
8155        let line_ctor = LineCtor {
8156            start: Point2d {
8157                x: Expr::Number(Number {
8158                    value: 1.0,
8159                    units: NumericSuffix::Mm,
8160                }),
8161                y: Expr::Number(Number {
8162                    value: 2.0,
8163                    units: NumericSuffix::Mm,
8164                }),
8165            },
8166            end: Point2d {
8167                x: Expr::Number(Number {
8168                    value: 13.0,
8169                    units: NumericSuffix::Mm,
8170                }),
8171                y: Expr::Number(Number {
8172                    value: 14.0,
8173                    units: NumericSuffix::Mm,
8174                }),
8175            },
8176            construction: None,
8177        };
8178        let segments = vec![ExistingSegmentCtor {
8179            id: line,
8180            ctor: SegmentCtor::Line(line_ctor),
8181        }];
8182        let (src_delta, scene_delta) = frontend
8183            .edit_segments(&mock_ctx, version, sketch_id, segments)
8184            .await
8185            .unwrap();
8186        insta::assert_snapshot!("test_new_sketch_add_line_edit_line_2", src_delta.text.as_str());
8187        assert_eq!(scene_delta.new_objects, vec![]);
8188        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8189
8190        ctx.close().await;
8191        mock_ctx.close().await;
8192    }
8193
8194    #[tokio::test(flavor = "multi_thread")]
8195    async fn test_new_sketch_add_arc_edit_arc() {
8196        let program = Program::empty();
8197
8198        let mut frontend = FrontendState::new();
8199        frontend.program = program;
8200
8201        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8202        let mock_ctx = ExecutorContext::new_mock(None).await;
8203        let version = Version(0);
8204
8205        let sketch_args = SketchCtor {
8206            on: Plane::Default(PlaneName::Xy),
8207        };
8208        let (_src_delta, scene_delta, sketch_id) = frontend
8209            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8210            .await
8211            .unwrap();
8212        assert_eq!(sketch_id, ObjectId(1));
8213        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8214        let sketch_object = &scene_delta.new_graph.objects[1];
8215        assert_eq!(sketch_object.id, ObjectId(1));
8216        assert_eq!(
8217            sketch_object.kind,
8218            ObjectKind::Sketch(Sketch {
8219                args: SketchCtor {
8220                    on: Plane::Default(PlaneName::Xy),
8221                },
8222                plane: ObjectId(0),
8223                segments: vec![],
8224                constraints: vec![],
8225            })
8226        );
8227        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8228
8229        let arc_ctor = ArcCtor {
8230            start: Point2d {
8231                x: Expr::Var(Number {
8232                    value: 0.0,
8233                    units: NumericSuffix::Mm,
8234                }),
8235                y: Expr::Var(Number {
8236                    value: 0.0,
8237                    units: NumericSuffix::Mm,
8238                }),
8239            },
8240            end: Point2d {
8241                x: Expr::Var(Number {
8242                    value: 10.0,
8243                    units: NumericSuffix::Mm,
8244                }),
8245                y: Expr::Var(Number {
8246                    value: 10.0,
8247                    units: NumericSuffix::Mm,
8248                }),
8249            },
8250            center: Point2d {
8251                x: Expr::Var(Number {
8252                    value: 10.0,
8253                    units: NumericSuffix::Mm,
8254                }),
8255                y: Expr::Var(Number {
8256                    value: 0.0,
8257                    units: NumericSuffix::Mm,
8258                }),
8259            },
8260            direction: None,
8261            construction: None,
8262        };
8263        let segment = SegmentCtor::Arc(arc_ctor);
8264        let (src_delta, scene_delta) = frontend
8265            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8266            .await
8267            .unwrap();
8268        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_1", src_delta.text.as_str());
8269        assert_eq!(
8270            scene_delta.new_objects,
8271            vec![ObjectId(2), ObjectId(3), ObjectId(4), ObjectId(5)]
8272        );
8273        for (i, scene_object) in scene_delta.new_graph.objects.iter().enumerate() {
8274            assert_eq!(scene_object.id.0, i);
8275        }
8276        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8277
8278        // The new objects are the end points, the center, and then the arc.
8279        let arc = *scene_delta.new_objects.last().unwrap();
8280
8281        let arc_ctor = ArcCtor {
8282            start: Point2d {
8283                x: Expr::Var(Number {
8284                    value: 1.0,
8285                    units: NumericSuffix::Mm,
8286                }),
8287                y: Expr::Var(Number {
8288                    value: 2.0,
8289                    units: NumericSuffix::Mm,
8290                }),
8291            },
8292            end: Point2d {
8293                x: Expr::Var(Number {
8294                    value: 13.0,
8295                    units: NumericSuffix::Mm,
8296                }),
8297                y: Expr::Var(Number {
8298                    value: 14.0,
8299                    units: NumericSuffix::Mm,
8300                }),
8301            },
8302            center: Point2d {
8303                x: Expr::Var(Number {
8304                    value: 13.0,
8305                    units: NumericSuffix::Mm,
8306                }),
8307                y: Expr::Var(Number {
8308                    value: 2.0,
8309                    units: NumericSuffix::Mm,
8310                }),
8311            },
8312            direction: None,
8313            construction: None,
8314        };
8315        let segments = vec![ExistingSegmentCtor {
8316            id: arc,
8317            ctor: SegmentCtor::Arc(arc_ctor),
8318        }];
8319        let (src_delta, scene_delta) = frontend
8320            .edit_segments(&mock_ctx, version, sketch_id, segments)
8321            .await
8322            .unwrap();
8323        insta::assert_snapshot!("test_new_sketch_add_arc_edit_arc_2", src_delta.text.as_str());
8324        assert_eq!(scene_delta.new_objects, vec![]);
8325        assert_eq!(scene_delta.new_graph.objects.len(), 6);
8326
8327        ctx.close().await;
8328        mock_ctx.close().await;
8329    }
8330
8331    #[tokio::test(flavor = "multi_thread")]
8332    async fn test_new_sketch_add_circle_edit_circle() {
8333        let program = Program::empty();
8334
8335        let mut frontend = FrontendState::new();
8336        frontend.program = program;
8337
8338        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8339        let mock_ctx = ExecutorContext::new_mock(None).await;
8340        let version = Version(0);
8341
8342        let sketch_args = SketchCtor {
8343            on: Plane::Default(PlaneName::Xy),
8344        };
8345        let (_src_delta, _scene_delta, sketch_id) = frontend
8346            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8347            .await
8348            .unwrap();
8349
8350        // Add a circle segment.
8351        let circle_ctor = CircleCtor {
8352            start: Point2d {
8353                x: Expr::Var(Number {
8354                    value: 5.0,
8355                    units: NumericSuffix::Mm,
8356                }),
8357                y: Expr::Var(Number {
8358                    value: 0.0,
8359                    units: NumericSuffix::Mm,
8360                }),
8361            },
8362            center: Point2d {
8363                x: Expr::Var(Number {
8364                    value: 0.0,
8365                    units: NumericSuffix::Mm,
8366                }),
8367                y: Expr::Var(Number {
8368                    value: 0.0,
8369                    units: NumericSuffix::Mm,
8370                }),
8371            },
8372            construction: None,
8373        };
8374        let segment = SegmentCtor::Circle(circle_ctor);
8375        let (src_delta, scene_delta) = frontend
8376            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8377            .await
8378            .unwrap();
8379        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_1", src_delta.text.as_str());
8380        // The new objects are start, center, and then the circle segment.
8381        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8382        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8383
8384        let circle = *scene_delta.new_objects.last().unwrap();
8385
8386        // Edit the circle segment.
8387        let circle_ctor = CircleCtor {
8388            start: Point2d {
8389                x: Expr::Var(Number {
8390                    value: 10.0,
8391                    units: NumericSuffix::Mm,
8392                }),
8393                y: Expr::Var(Number {
8394                    value: 0.0,
8395                    units: NumericSuffix::Mm,
8396                }),
8397            },
8398            center: Point2d {
8399                x: Expr::Var(Number {
8400                    value: 3.0,
8401                    units: NumericSuffix::Mm,
8402                }),
8403                y: Expr::Var(Number {
8404                    value: 4.0,
8405                    units: NumericSuffix::Mm,
8406                }),
8407            },
8408            construction: None,
8409        };
8410        let segments = vec![ExistingSegmentCtor {
8411            id: circle,
8412            ctor: SegmentCtor::Circle(circle_ctor),
8413        }];
8414        let (src_delta, scene_delta) = frontend
8415            .edit_segments(&mock_ctx, version, sketch_id, segments)
8416            .await
8417            .unwrap();
8418        insta::assert_snapshot!("test_new_sketch_add_circle_edit_circle_2", src_delta.text.as_str());
8419        assert_eq!(scene_delta.new_objects, vec![]);
8420        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8421
8422        ctx.close().await;
8423        mock_ctx.close().await;
8424    }
8425
8426    #[tokio::test(flavor = "multi_thread")]
8427    async fn test_delete_circle() {
8428        let initial_source = "sketch001 = sketch(on = XY) {
8429  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8430}
8431";
8432
8433        let program = Program::parse(initial_source).unwrap().0.unwrap();
8434        let mut frontend = FrontendState::new();
8435
8436        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8437        let mock_ctx = ExecutorContext::new_mock(None).await;
8438        let version = Version(0);
8439
8440        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8441        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8442        let sketch_id = sketch_object.id;
8443        let sketch = expect_sketch(sketch_object);
8444
8445        // The sketch should have 3 segments: start point, center point, and the circle.
8446        assert_eq!(sketch.segments.len(), 3);
8447        let circle_id = sketch.segments[2];
8448
8449        // Delete the circle.
8450        let (src_delta, scene_delta) = frontend
8451            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
8452            .await
8453            .unwrap();
8454        insta::assert_snapshot!("test_delete_circle", src_delta.text.as_str());
8455        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
8456        let new_sketch = expect_sketch(new_sketch_object);
8457        assert_eq!(new_sketch.segments.len(), 0);
8458
8459        ctx.close().await;
8460        mock_ctx.close().await;
8461    }
8462
8463    #[tokio::test(flavor = "multi_thread")]
8464    async fn test_edit_circle_via_point() {
8465        let initial_source = "sketch001 = sketch(on = XY) {
8466  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
8467}
8468";
8469
8470        let program = Program::parse(initial_source).unwrap().0.unwrap();
8471        let mut frontend = FrontendState::new();
8472
8473        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8474        let mock_ctx = ExecutorContext::new_mock(None).await;
8475        let version = Version(0);
8476
8477        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8478        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8479        let sketch_id = sketch_object.id;
8480        let sketch = expect_sketch(sketch_object);
8481
8482        // Find the circle segment and its start point.
8483        let circle_id = sketch
8484            .segments
8485            .iter()
8486            .copied()
8487            .find(|seg_id| {
8488                matches!(
8489                    &frontend.scene_graph.objects[seg_id.0].kind,
8490                    ObjectKind::Segment {
8491                        segment: Segment::Circle(_)
8492                    }
8493                )
8494            })
8495            .expect("Expected a circle segment in sketch");
8496        let circle_object = &frontend.scene_graph.objects[circle_id.0];
8497        let ObjectKind::Segment {
8498            segment: Segment::Circle(circle),
8499        } = &circle_object.kind
8500        else {
8501            panic!("Expected circle segment, got: {:?}", circle_object.kind);
8502        };
8503        let start_point_id = circle.start;
8504
8505        // Edit the start point via SegmentCtor::Point.
8506        let segments = vec![ExistingSegmentCtor {
8507            id: start_point_id,
8508            ctor: SegmentCtor::Point(PointCtor {
8509                position: Point2d {
8510                    x: Expr::Var(Number {
8511                        value: 7.0,
8512                        units: NumericSuffix::Mm,
8513                    }),
8514                    y: Expr::Var(Number {
8515                        value: 1.0,
8516                        units: NumericSuffix::Mm,
8517                    }),
8518                },
8519            }),
8520        }];
8521        let (src_delta, _scene_delta) = frontend
8522            .edit_segments(&mock_ctx, version, sketch_id, segments)
8523            .await
8524            .unwrap();
8525        insta::assert_snapshot!("test_edit_circle_via_point", src_delta.text.as_str());
8526
8527        ctx.close().await;
8528        mock_ctx.close().await;
8529    }
8530
8531    #[tokio::test(flavor = "multi_thread")]
8532    async fn test_add_line_when_sketch_block_uses_variable() {
8533        let initial_source = "s = sketch(on = XY) {}
8534";
8535
8536        let program = Program::parse(initial_source).unwrap().0.unwrap();
8537
8538        let mut frontend = FrontendState::new();
8539
8540        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8541        let mock_ctx = ExecutorContext::new_mock(None).await;
8542        let version = Version(0);
8543
8544        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8545        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8546        let sketch_id = sketch_object.id;
8547
8548        let line_ctor = LineCtor {
8549            start: Point2d {
8550                x: Expr::Number(Number {
8551                    value: 0.0,
8552                    units: NumericSuffix::Mm,
8553                }),
8554                y: Expr::Number(Number {
8555                    value: 0.0,
8556                    units: NumericSuffix::Mm,
8557                }),
8558            },
8559            end: Point2d {
8560                x: Expr::Number(Number {
8561                    value: 10.0,
8562                    units: NumericSuffix::Mm,
8563                }),
8564                y: Expr::Number(Number {
8565                    value: 10.0,
8566                    units: NumericSuffix::Mm,
8567                }),
8568            },
8569            construction: None,
8570        };
8571        let segment = SegmentCtor::Line(line_ctor);
8572        let (src_delta, scene_delta) = frontend
8573            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8574            .await
8575            .unwrap();
8576        insta::assert_snapshot!("test_add_line_when_sketch_block_uses_variable", src_delta.text.as_str());
8577        assert_eq!(scene_delta.new_objects, vec![ObjectId(2), ObjectId(3), ObjectId(4)]);
8578        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8579
8580        ctx.close().await;
8581        mock_ctx.close().await;
8582    }
8583
8584    #[tokio::test(flavor = "multi_thread")]
8585    async fn test_new_sketch_add_line_delete_sketch() {
8586        let program = Program::empty();
8587
8588        let mut frontend = FrontendState::new();
8589        frontend.program = program;
8590
8591        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8592        let mock_ctx = ExecutorContext::new_mock(None).await;
8593        let version = Version(0);
8594
8595        let sketch_args = SketchCtor {
8596            on: Plane::Default(PlaneName::Xy),
8597        };
8598        let (_src_delta, scene_delta, sketch_id) = frontend
8599            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
8600            .await
8601            .unwrap();
8602        assert_eq!(sketch_id, ObjectId(1));
8603        assert_eq!(scene_delta.new_objects, vec![ObjectId(1)]);
8604        let sketch_object = &scene_delta.new_graph.objects[1];
8605        assert_eq!(sketch_object.id, ObjectId(1));
8606        assert_eq!(
8607            sketch_object.kind,
8608            ObjectKind::Sketch(Sketch {
8609                args: SketchCtor {
8610                    on: Plane::Default(PlaneName::Xy)
8611                },
8612                plane: ObjectId(0),
8613                segments: vec![],
8614                constraints: vec![],
8615            })
8616        );
8617        assert_eq!(scene_delta.new_graph.objects.len(), 2);
8618
8619        let line_ctor = LineCtor {
8620            start: Point2d {
8621                x: Expr::Number(Number {
8622                    value: 0.0,
8623                    units: NumericSuffix::Mm,
8624                }),
8625                y: Expr::Number(Number {
8626                    value: 0.0,
8627                    units: NumericSuffix::Mm,
8628                }),
8629            },
8630            end: Point2d {
8631                x: Expr::Number(Number {
8632                    value: 10.0,
8633                    units: NumericSuffix::Mm,
8634                }),
8635                y: Expr::Number(Number {
8636                    value: 10.0,
8637                    units: NumericSuffix::Mm,
8638                }),
8639            },
8640            construction: None,
8641        };
8642        let segment = SegmentCtor::Line(line_ctor);
8643        let (src_delta, scene_delta) = frontend
8644            .add_segment(&mock_ctx, version, sketch_id, segment, None)
8645            .await
8646            .unwrap();
8647        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_1", src_delta.text.as_str());
8648        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8649
8650        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8651        insta::assert_snapshot!("test_new_sketch_add_line_delete_sketch_2", src_delta.text.as_str());
8652        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8653
8654        ctx.close().await;
8655        mock_ctx.close().await;
8656    }
8657
8658    #[tokio::test(flavor = "multi_thread")]
8659    async fn test_delete_sketch_when_sketch_block_uses_variable() {
8660        let initial_source = "s = sketch(on = XY) {}
8661";
8662
8663        let program = Program::parse(initial_source).unwrap().0.unwrap();
8664
8665        let mut frontend = FrontendState::new();
8666
8667        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8668        let version = Version(0);
8669
8670        frontend.hack_set_program(&ctx, program).await.unwrap();
8671        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8672        let sketch_id = sketch_object.id;
8673
8674        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8675        insta::assert_snapshot!(
8676            "test_delete_sketch_when_sketch_block_uses_variable",
8677            src_delta.text.as_str()
8678        );
8679        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8680
8681        ctx.close().await;
8682    }
8683
8684    #[tokio::test(flavor = "multi_thread")]
8685    async fn test_delete_sketch_after_comment() {
8686        let initial_source = "sketch001 = sketch(on = XZ) {
8687}
8688";
8689
8690        let program = Program::parse(initial_source).unwrap().0.unwrap();
8691        let mut frontend = FrontendState::new();
8692
8693        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8694        let version = Version(0);
8695
8696        frontend.hack_set_program(&ctx, program).await.unwrap();
8697        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8698        let sketch_id = sketch_object.id;
8699        let original_source = sketch_object.source.clone();
8700
8701        let commented_source = "// test 1
8702sketch001 = sketch(on = XZ) {
8703}
8704";
8705        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8706        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8707
8708        let cached_sketch_object = &frontend.scene_graph.objects[sketch_id.0];
8709        assert_eq!(cached_sketch_object.source, original_source);
8710
8711        let (src_delta, scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8712        assert!(
8713            !src_delta.text.contains("sketch001"),
8714            "sketch was not deleted: {}",
8715            src_delta.text
8716        );
8717        // The leading line comment must survive deletion.
8718        insta::assert_snapshot!("test_delete_sketch_after_comment", src_delta.text.as_str());
8719        assert_eq!(scene_delta.new_graph.objects.len(), 0);
8720
8721        ctx.close().await;
8722    }
8723
8724    #[tokio::test(flavor = "multi_thread")]
8725    async fn test_delete_sketch_preserves_pre_comment_when_followed_by_code() {
8726        let initial_source = "sketch001 = sketch(on = XZ) {
8727}
8728foo = 1
8729";
8730
8731        let program = Program::parse(initial_source).unwrap().0.unwrap();
8732        let mut frontend = FrontendState::new();
8733
8734        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
8735        let version = Version(0);
8736
8737        frontend.hack_set_program(&ctx, program).await.unwrap();
8738        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8739        let sketch_id = sketch_object.id;
8740
8741        let commented_source = "// keep me
8742sketch001 = sketch(on = XZ) {
8743}
8744foo = 1
8745";
8746        let commented_program = Program::parse(commented_source).unwrap().0.unwrap();
8747        frontend.engine_execute(&ctx, commented_program).await.unwrap();
8748
8749        let (src_delta, _scene_delta) = frontend.delete_sketch(&ctx, version, sketch_id).await.unwrap();
8750        // The leading comment should remain, now attached to the following body item.
8751        insta::assert_snapshot!(
8752            "test_delete_sketch_preserves_pre_comment_when_followed_by_code",
8753            src_delta.text.as_str()
8754        );
8755
8756        ctx.close().await;
8757    }
8758
8759    #[tokio::test(flavor = "multi_thread")]
8760    async fn test_delete_segment_preserves_pre_comment() {
8761        let initial_source = "\
8762sketch(on = XY) {
8763  point(at = [var 1, var 2])
8764  // describe the middle point
8765  point(at = [var 3, var 4])
8766  point(at = [var 5, var 6])
8767}
8768";
8769
8770        let program = Program::parse(initial_source).unwrap().0.unwrap();
8771        let mut frontend = FrontendState::new();
8772
8773        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8774        let mock_ctx = ExecutorContext::new_mock(None).await;
8775        let version = Version(0);
8776
8777        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8778        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8779        let sketch_id = sketch_object.id;
8780        let sketch = expect_sketch(sketch_object);
8781
8782        let middle_point_id = *sketch.segments.get(1).unwrap();
8783
8784        let (src_delta, _scene_delta) = frontend
8785            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8786            .await
8787            .unwrap();
8788        // The line comment on the line above the deleted point must be preserved.
8789        // It is reattached to the next surviving body item.
8790        insta::assert_snapshot!("test_delete_segment_preserves_pre_comment", src_delta.text.as_str());
8791
8792        ctx.close().await;
8793        mock_ctx.close().await;
8794    }
8795
8796    #[tokio::test(flavor = "multi_thread")]
8797    async fn test_delete_last_segment_preserves_pre_comment() {
8798        let initial_source = "\
8799sketch(on = XY) {
8800  point(at = [var 1, var 2])
8801  // describe the trailing point
8802  point(at = [var 3, var 4])
8803}
8804";
8805
8806        let program = Program::parse(initial_source).unwrap().0.unwrap();
8807        let mut frontend = FrontendState::new();
8808
8809        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8810        let mock_ctx = ExecutorContext::new_mock(None).await;
8811        let version = Version(0);
8812
8813        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8814        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8815        let sketch_id = sketch_object.id;
8816        let sketch = expect_sketch(sketch_object);
8817
8818        let last_point_id = *sketch.segments.last().unwrap();
8819
8820        let (src_delta, _scene_delta) = frontend
8821            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![last_point_id])
8822            .await
8823            .unwrap();
8824        // No following item to attach to; the comment is kept inside the sketch
8825        // block as trailing non-code metadata so the user does not lose it.
8826        insta::assert_snapshot!(
8827            "test_delete_last_segment_preserves_pre_comment",
8828            src_delta.text.as_str()
8829        );
8830
8831        ctx.close().await;
8832        mock_ctx.close().await;
8833    }
8834
8835    #[tokio::test(flavor = "multi_thread")]
8836    async fn test_delete_segment_drops_inline_trailing_comment() {
8837        let initial_source = "\
8838sketch(on = XY) {
8839  point(at = [var 1, var 2])
8840  point(at = [var 3, var 4]) // same-line note that gets dropped
8841  point(at = [var 5, var 6])
8842}
8843";
8844
8845        let program = Program::parse(initial_source).unwrap().0.unwrap();
8846        let mut frontend = FrontendState::new();
8847
8848        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8849        let mock_ctx = ExecutorContext::new_mock(None).await;
8850        let version = Version(0);
8851
8852        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8853        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8854        let sketch_id = sketch_object.id;
8855        let sketch = expect_sketch(sketch_object);
8856
8857        let middle_point_id = *sketch.segments.get(1).unwrap();
8858
8859        let (src_delta, _scene_delta) = frontend
8860            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![middle_point_id])
8861            .await
8862            .unwrap();
8863        // The same-line trailing comment is removed along with the deleted code.
8864        assert!(
8865            !src_delta.text.contains("same-line note"),
8866            "inline comment should have been removed: {}",
8867            src_delta.text
8868        );
8869
8870        ctx.close().await;
8871        mock_ctx.close().await;
8872    }
8873
8874    #[tokio::test(flavor = "multi_thread")]
8875    async fn test_delete_segments_preserves_block_comments_across_positions() {
8876        // One test exercising several `delete_body_item_preserving_pre_comments`
8877        // branches at once with `/* ... */` block comments:
8878        //   - first point: leading block comment must migrate to the next item.
8879        //   - first point: same-line trailing block comment must be dropped.
8880        //   - middle point: leading block comment must stay attached after migration.
8881        //   - last point: leading block comment, with no surviving next item,
8882        //     must be converted into a trailing NonCodeNode.
8883        let initial_source = "\
8884sketch(on = XY) {
8885  /* above first - moves to middle */
8886  point(at = [var 1, var 2]) /* same-line on first - dropped */
8887  /* above middle - stays */
8888  point(at = [var 3, var 4])
8889  /* above last - moves to trailing meta */
8890  point(at = [var 5, var 6])
8891}
8892";
8893
8894        let program = Program::parse(initial_source).unwrap().0.unwrap();
8895        let mut frontend = FrontendState::new();
8896
8897        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8898        let mock_ctx = ExecutorContext::new_mock(None).await;
8899        let version = Version(0);
8900
8901        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8902        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8903        let sketch_id = sketch_object.id;
8904        let sketch = expect_sketch(sketch_object);
8905
8906        let first_point_id = *sketch.segments.first().unwrap();
8907        let last_point_id = *sketch.segments.last().unwrap();
8908
8909        let (src_delta, _scene_delta) = frontend
8910            .delete_objects(
8911                &mock_ctx,
8912                version,
8913                sketch_id,
8914                Vec::new(),
8915                vec![first_point_id, last_point_id],
8916            )
8917            .await
8918            .unwrap();
8919        insta::assert_snapshot!(
8920            "test_delete_segments_preserves_block_comments_across_positions",
8921            src_delta.text.as_str()
8922        );
8923
8924        ctx.close().await;
8925        mock_ctx.close().await;
8926    }
8927
8928    #[tokio::test(flavor = "multi_thread")]
8929    async fn test_edit_line_when_editing_its_start_point() {
8930        let initial_source = "\
8931sketch(on = XY) {
8932  line(start = [var 1, var 2], end = [var 3, var 4])
8933}
8934";
8935
8936        let program = Program::parse(initial_source).unwrap().0.unwrap();
8937
8938        let mut frontend = FrontendState::new();
8939
8940        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8941        let mock_ctx = ExecutorContext::new_mock(None).await;
8942        let version = Version(0);
8943
8944        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8945        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8946        let sketch_id = sketch_object.id;
8947        let sketch = expect_sketch(sketch_object);
8948
8949        let point_id = *sketch.segments.first().unwrap();
8950
8951        let point_ctor = PointCtor {
8952            position: Point2d {
8953                x: Expr::Var(Number {
8954                    value: 5.0,
8955                    units: NumericSuffix::Inch,
8956                }),
8957                y: Expr::Var(Number {
8958                    value: 6.0,
8959                    units: NumericSuffix::Inch,
8960                }),
8961            },
8962        };
8963        let segments = vec![ExistingSegmentCtor {
8964            id: point_id,
8965            ctor: SegmentCtor::Point(point_ctor),
8966        }];
8967        let (src_delta, scene_delta) = frontend
8968            .edit_segments(&mock_ctx, version, sketch_id, segments)
8969            .await
8970            .unwrap();
8971        insta::assert_snapshot!("test_edit_line_when_editing_its_start_point", src_delta.text.as_str());
8972        assert_eq!(scene_delta.new_objects, vec![]);
8973        assert_eq!(scene_delta.new_graph.objects.len(), 5);
8974
8975        ctx.close().await;
8976        mock_ctx.close().await;
8977    }
8978
8979    #[tokio::test(flavor = "multi_thread")]
8980    async fn test_edit_line_when_editing_its_end_point() {
8981        let initial_source = "\
8982sketch(on = XY) {
8983  line(start = [var 1, var 2], end = [var 3, var 4])
8984}
8985";
8986
8987        let program = Program::parse(initial_source).unwrap().0.unwrap();
8988
8989        let mut frontend = FrontendState::new();
8990
8991        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
8992        let mock_ctx = ExecutorContext::new_mock(None).await;
8993        let version = Version(0);
8994
8995        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
8996        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
8997        let sketch_id = sketch_object.id;
8998        let sketch = expect_sketch(sketch_object);
8999        let point_id = *sketch.segments.get(1).unwrap();
9000
9001        let point_ctor = PointCtor {
9002            position: Point2d {
9003                x: Expr::Var(Number {
9004                    value: 5.0,
9005                    units: NumericSuffix::Inch,
9006                }),
9007                y: Expr::Var(Number {
9008                    value: 6.0,
9009                    units: NumericSuffix::Inch,
9010                }),
9011            },
9012        };
9013        let segments = vec![ExistingSegmentCtor {
9014            id: point_id,
9015            ctor: SegmentCtor::Point(point_ctor),
9016        }];
9017        let (src_delta, scene_delta) = frontend
9018            .edit_segments(&mock_ctx, version, sketch_id, segments)
9019            .await
9020            .unwrap();
9021        insta::assert_snapshot!("test_edit_line_when_editing_its_end_point", src_delta.text.as_str());
9022        assert_eq!(scene_delta.new_objects, vec![]);
9023        assert_eq!(
9024            scene_delta.new_graph.objects.len(),
9025            5,
9026            "{:#?}",
9027            scene_delta.new_graph.objects
9028        );
9029
9030        ctx.close().await;
9031        mock_ctx.close().await;
9032    }
9033
9034    #[tokio::test(flavor = "multi_thread")]
9035    async fn test_edit_line_with_coincident_feedback() {
9036        let initial_source = "\
9037sketch(on = XY) {
9038  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9039  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9040  fixed([line1.start, [0, 0]])
9041  coincident([line1.end, line2.start])
9042  equalLength([line1, line2])
9043}
9044";
9045
9046        let program = Program::parse(initial_source).unwrap().0.unwrap();
9047
9048        let mut frontend = FrontendState::new();
9049
9050        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9051        let mock_ctx = ExecutorContext::new_mock(None).await;
9052        let version = Version(0);
9053
9054        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9055        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9056        let sketch_id = sketch_object.id;
9057        let sketch = expect_sketch(sketch_object);
9058        let line2_end_id = *sketch.segments.get(4).unwrap();
9059
9060        let segments = vec![ExistingSegmentCtor {
9061            id: line2_end_id,
9062            ctor: SegmentCtor::Point(PointCtor {
9063                position: Point2d {
9064                    x: Expr::Var(Number {
9065                        value: 9.0,
9066                        units: NumericSuffix::None,
9067                    }),
9068                    y: Expr::Var(Number {
9069                        value: 10.0,
9070                        units: NumericSuffix::None,
9071                    }),
9072                },
9073            }),
9074        }];
9075        let (src_delta, scene_delta) = frontend
9076            .edit_segments(&mock_ctx, version, sketch_id, segments)
9077            .await
9078            .unwrap();
9079        insta::assert_snapshot!("test_edit_line_with_coincident_feedback", src_delta.text.as_str());
9080        assert_eq!(
9081            scene_delta.new_graph.objects.len(),
9082            11,
9083            "{:#?}",
9084            scene_delta.new_graph.objects
9085        );
9086
9087        ctx.close().await;
9088        mock_ctx.close().await;
9089    }
9090
9091    #[tokio::test(flavor = "multi_thread")]
9092    async fn test_edit_segments_persists_solver_feedback_for_next_mock_execute() {
9093        let initial_source = "\
9094sketch(on = XY) {
9095  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9096  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9097  fixed([line1.start, [0, 0]])
9098  coincident([line1.end, line2.start])
9099  equalLength([line1, line2])
9100}
9101";
9102
9103        let program = Program::parse(initial_source).unwrap().0.unwrap();
9104        let mut frontend = FrontendState::new();
9105        let mock_ctx = ExecutorContext::new_mock(None).await;
9106        let version = Version(0);
9107
9108        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9109        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9110        let sketch_id = sketch_object.id;
9111        let sketch = expect_sketch(sketch_object);
9112        let line2_end_id = *sketch.segments.get(4).unwrap();
9113
9114        let segments = vec![ExistingSegmentCtor {
9115            id: line2_end_id,
9116            ctor: SegmentCtor::Point(PointCtor {
9117                position: Point2d {
9118                    x: Expr::Var(Number {
9119                        value: 9.0,
9120                        units: NumericSuffix::None,
9121                    }),
9122                    y: Expr::Var(Number {
9123                        value: 10.0,
9124                        units: NumericSuffix::None,
9125                    }),
9126                },
9127            }),
9128        }];
9129        let (edited_source, _) = frontend
9130            .edit_segments(&mock_ctx, version, sketch_id, segments)
9131            .await
9132            .unwrap();
9133
9134        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9135        assert_eq!(mock_source.text, edited_source.text);
9136
9137        mock_ctx.close().await;
9138    }
9139
9140    /// Preview segment edits should return solved geometry without persisting
9141    /// solver feedback to KCL.
9142    #[tokio::test(flavor = "multi_thread")]
9143    async fn test_preview_edit_segments_does_not_persist_solver_feedback() {
9144        let initial_source = "\
9145sketch(on = XY) {
9146  line1 = line(start = [var 1, var 2], end = [var 1, var 2])
9147  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9148  fixed([line1.start, [0, 0]])
9149  coincident([line1.end, line2.start])
9150  equalLength([line1, line2])
9151}
9152";
9153
9154        let program = Program::parse(initial_source).unwrap().0.unwrap();
9155        let mut frontend = FrontendState::new();
9156        let mock_ctx = ExecutorContext::new_mock(None).await;
9157        let version = Version(0);
9158
9159        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9160        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9161        let sketch_id = sketch_object.id;
9162        let sketch = expect_sketch(sketch_object);
9163        let line2_end_id = *sketch.segments.get(4).unwrap();
9164
9165        let segments = vec![ExistingSegmentCtor {
9166            id: line2_end_id,
9167            ctor: SegmentCtor::Point(PointCtor {
9168                position: Point2d {
9169                    x: Expr::Var(Number {
9170                        value: 9.0,
9171                        units: NumericSuffix::None,
9172                    }),
9173                    y: Expr::Var(Number {
9174                        value: 10.0,
9175                        units: NumericSuffix::None,
9176                    }),
9177                },
9178            }),
9179        }];
9180        let (preview_source, preview_delta) = frontend
9181            .edit_segments_with_options(
9182                &mock_ctx,
9183                version,
9184                sketch_id,
9185                segments,
9186                EditSegmentsOptions {
9187                    anchor_segment_ids: Some(vec![line2_end_id]),
9188                    drag_anchors: Vec::new(),
9189                    constraint_label_edits: Vec::new(),
9190                    commit_solved_initial_guesses: false,
9191                },
9192            )
9193            .await
9194            .unwrap();
9195
9196        assert!(
9197            !preview_delta.exec_outcome.var_solutions.is_empty(),
9198            "preview solve should still solve and return geometry feedback"
9199        );
9200        assert!(
9201            preview_source
9202                .text
9203                .contains("line1 = line(start = [var 1, var 2], end = [var 1, var 2])")
9204        );
9205        assert!(
9206            preview_source
9207                .text
9208                .contains("line2 = line(start = [var 5, var 6], end = [var 9, var 10])")
9209        );
9210
9211        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9212        assert_eq!(mock_source.text, preview_source.text);
9213
9214        mock_ctx.close().await;
9215    }
9216
9217    #[tokio::test(flavor = "multi_thread")]
9218    async fn test_add_constraint_persists_solver_feedback_for_next_mock_execute() {
9219        let initial_source = "\
9220sketch(on = XY) {
9221  line1 = line(start = [var 0, var 0], end = [var 10, var 0])
9222}
9223";
9224
9225        let program = Program::parse(initial_source).unwrap().0.unwrap();
9226        let mut frontend = FrontendState::new();
9227        let mock_ctx = ExecutorContext::new_mock(None).await;
9228        let version = Version(0);
9229
9230        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9231        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9232        let sketch_id = sketch_object.id;
9233        let sketch = expect_sketch(sketch_object);
9234        let line_end_id = *sketch.segments.get(1).unwrap();
9235
9236        let constraint = Constraint::Fixed(Fixed {
9237            points: vec![FixedPoint {
9238                point: line_end_id,
9239                position: Point2d {
9240                    x: Number {
9241                        value: 20.0,
9242                        units: NumericSuffix::Mm,
9243                    },
9244                    y: Number {
9245                        value: 0.0,
9246                        units: NumericSuffix::Mm,
9247                    },
9248                },
9249            }],
9250        });
9251        let (constraint_source, _) = frontend
9252            .add_constraint(&mock_ctx, version, sketch_id, constraint)
9253            .await
9254            .unwrap();
9255
9256        assert!(
9257            constraint_source
9258                .text
9259                .contains("line1 = line(start = [var 0, var 0], end = [var 20, var 0])"),
9260            "{}",
9261            constraint_source.text
9262        );
9263        let (mock_source, _) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
9264        assert_eq!(mock_source.text, constraint_source.text);
9265
9266        mock_ctx.close().await;
9267    }
9268
9269    #[test]
9270    fn test_no_solver_feedback_preserves_original_source() {
9271        let initial_source = "\
9272@settings(defaultLengthUnit = in, kclVersion = 2.0)
9273cylinder = startSketchOn(XY)
9274    |> circle(center= [0, 0], radius= 22)
9275    |> extrude(length = 14)
9276";
9277        let mut frontend = FrontendState::new();
9278        frontend.program = Program::parse(initial_source).unwrap().0.unwrap();
9279        let outcome = ExecOutcome {
9280            variables: Default::default(),
9281            test_program_memory: Default::default(),
9282            operations: Default::default(),
9283            artifact_graph: Default::default(),
9284            scene_objects: Default::default(),
9285            source_range_to_object: Default::default(),
9286            var_solutions: Default::default(),
9287            refactor_metadata: Default::default(),
9288            issues: Default::default(),
9289            filenames: Default::default(),
9290            source_files: Default::default(),
9291            default_planes: Default::default(),
9292        };
9293
9294        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9295
9296        assert_eq!(source_delta.text, initial_source);
9297    }
9298
9299    /// Explicit drag anchors should limit which edited points become temporary
9300    /// fixed constraints.
9301    #[tokio::test(flavor = "multi_thread")]
9302    async fn test_edit_segments_with_anchor_ids_limits_drag_fixed_constraints() {
9303        let initial_source = "\
9304sketch(on = XY) {
9305  point1 = point(at = [var 0mm, var 0mm])
9306  point2 = point(at = [var 0mm, var 0mm])
9307  coincident([point1, point2])
9308}
9309";
9310
9311        let program = Program::parse(initial_source).unwrap().0.unwrap();
9312        let mut frontend = FrontendState::new();
9313        let mock_ctx = ExecutorContext::new_mock(None).await;
9314        let version = Version(0);
9315
9316        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9317        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9318        let sketch_id = sketch_object.id;
9319        let sketch = expect_sketch(sketch_object);
9320        let point1_id = sketch.segments[0];
9321        let point2_id = sketch.segments[1];
9322
9323        let segments = vec![
9324            ExistingSegmentCtor {
9325                id: point1_id,
9326                ctor: SegmentCtor::Point(PointCtor {
9327                    position: point_expr_mm(10.0, 0.0),
9328                }),
9329            },
9330            ExistingSegmentCtor {
9331                id: point2_id,
9332                ctor: SegmentCtor::Point(PointCtor {
9333                    position: point_expr_mm(100.0, 0.0),
9334                }),
9335            },
9336        ];
9337        let (_, scene_delta) = frontend
9338            .edit_segments_with_options(
9339                &mock_ctx,
9340                version,
9341                sketch_id,
9342                segments,
9343                EditSegmentsOptions {
9344                    anchor_segment_ids: Some(vec![point1_id]),
9345                    drag_anchors: Vec::new(),
9346                    constraint_label_edits: Vec::new(),
9347                    commit_solved_initial_guesses: true,
9348                },
9349            )
9350            .await
9351            .unwrap();
9352
9353        assert_point_position_close(
9354            point_position(&scene_delta.new_graph, point1_id),
9355            point_number_mm(10.0, 0.0),
9356        );
9357        assert_point_position_close(
9358            point_position(&scene_delta.new_graph, point2_id),
9359            point_number_mm(10.0, 0.0),
9360        );
9361
9362        mock_ctx.close().await;
9363    }
9364
9365    /// Walks a program collecting `(literal_source_range, sketch_var_node_path)`
9366    /// for every SketchVar whose initial NumericLiteral has the given value.
9367    fn collect_sketch_var_literals_with_value(program: &Program, value: f64) -> Vec<(SourceRange, ast::NodePath)> {
9368        use std::cell::RefCell;
9369        struct Collector {
9370            target: f64,
9371            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9372        }
9373        impl<'a> crate::walk::Visitor<'a> for &Collector {
9374            type Error = crate::front::Error;
9375            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9376                if let crate::walk::Node::SketchVar(sketch_var) = node
9377                    && let (Some(initial), Some(node_path)) = (&sketch_var.initial, &sketch_var.node_path)
9378                    && (initial.value - self.target).abs() < 1e-9
9379                {
9380                    self.out
9381                        .borrow_mut()
9382                        .push((SourceRange::from(initial.as_ref()), node_path.clone()));
9383                }
9384                for child in node.children().iter() {
9385                    if !child.visit(*self)? {
9386                        return Ok(false);
9387                    }
9388                }
9389                Ok(true)
9390            }
9391        }
9392        let collector = Collector {
9393            target: value,
9394            out: Default::default(),
9395        };
9396        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9397        collector.out.into_inner()
9398    }
9399
9400    /// Walk a program collecting `(sketch_var_source_range, sketch_var_node_path)`
9401    /// for every SketchVar (including bare `var`).
9402    fn collect_all_sketch_vars(program: &Program) -> Vec<(SourceRange, ast::NodePath)> {
9403        use std::cell::RefCell;
9404        struct Collector {
9405            out: RefCell<Vec<(SourceRange, ast::NodePath)>>,
9406        }
9407        impl<'a> crate::walk::Visitor<'a> for &Collector {
9408            type Error = crate::front::Error;
9409            fn visit_node(&self, node: crate::walk::Node<'a>) -> anyhow::Result<bool, Self::Error> {
9410                if let crate::walk::Node::SketchVar(sketch_var) = node
9411                    && let Some(node_path) = &sketch_var.node_path
9412                {
9413                    self.out
9414                        .borrow_mut()
9415                        .push((SourceRange::from(sketch_var), node_path.clone()));
9416                }
9417                for child in node.children().iter() {
9418                    if !child.visit(*self)? {
9419                        return Ok(false);
9420                    }
9421                }
9422                Ok(true)
9423            }
9424        }
9425        let collector = Collector {
9426            out: Default::default(),
9427        };
9428        let _ = crate::walk::Node::from(&program.ast).visit(&collector);
9429        collector.out.into_inner()
9430    }
9431
9432    fn empty_exec_outcome_with_var_solutions(
9433        var_solutions: Vec<(SourceRange, Option<ast::NodePath>, Number)>,
9434    ) -> ExecOutcome {
9435        ExecOutcome {
9436            variables: Default::default(),
9437            test_program_memory: Default::default(),
9438            operations: Default::default(),
9439            artifact_graph: Default::default(),
9440            scene_objects: Default::default(),
9441            source_range_to_object: Default::default(),
9442            var_solutions,
9443            refactor_metadata: Default::default(),
9444            issues: Default::default(),
9445            filenames: Default::default(),
9446            source_files: Default::default(),
9447            default_planes: Default::default(),
9448        }
9449    }
9450
9451    /// Happy path: commit a var solution to a `var N` inside a sketch block
9452    /// using a correct NodePath. Confirms the node-path code path produces the
9453    /// expected source mutation.
9454    #[test]
9455    fn test_commit_var_solution_by_node_path_updates_sketch_var() {
9456        let initial_source = "\
9457sketch(on = XY) {
9458  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9459}
9460";
9461        let program = Program::parse(initial_source).unwrap().0.unwrap();
9462        let matches = collect_sketch_var_literals_with_value(&program, 10.0);
9463        assert_eq!(matches.len(), 1, "expected exactly one `var 10mm`");
9464        let (literal_range, node_path) = matches.into_iter().next().unwrap();
9465
9466        let mut frontend = FrontendState::new();
9467        frontend.program = program;
9468
9469        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9470            literal_range,
9471            Some(node_path),
9472            Number {
9473                value: 25.0,
9474                units: NumericSuffix::Mm,
9475            },
9476        )]);
9477
9478        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9479
9480        insta::assert_snapshot!(
9481            "test_commit_var_solution_by_node_path_updates_sketch_var",
9482            source_delta.text
9483        );
9484    }
9485
9486    /// Whitespace inserted earlier in the source shifts the original SketchVar
9487    /// SourceRange. With NodePath propagation the commit should still target
9488    /// the right `var`. We simulate this by collecting node_paths against a
9489    /// "compact" source, then loading the frontend with a "padded" source
9490    /// (whose byte offsets differ), and feeding the original (now stale)
9491    /// source range plus the correct node_path back into the commit.
9492    #[test]
9493    fn test_commit_var_solution_survives_whitespace_shift_earlier_in_file() {
9494        let compact_source = "\
9495sketch(on = XY) {
9496  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9497}
9498";
9499        let padded_source = "\
9500// added comment\n// added comment\n\nsketch(on = XY) {
9501  line1 = line(start = [var 0, var 0], end = [var 10mm, var 0])
9502}
9503";
9504        let compact_program = Program::parse(compact_source).unwrap().0.unwrap();
9505        let padded_program = Program::parse(padded_source).unwrap().0.unwrap();
9506
9507        let compact_match = collect_sketch_var_literals_with_value(&compact_program, 10.0)
9508            .into_iter()
9509            .next()
9510            .expect("expected `var 10mm` in compact source");
9511        let padded_match = collect_sketch_var_literals_with_value(&padded_program, 10.0)
9512            .into_iter()
9513            .next()
9514            .expect("expected `var 10mm` in padded source");
9515
9516        assert_ne!(
9517            compact_match.0, padded_match.0,
9518            "byte offsets must differ for this test to be meaningful"
9519        );
9520        assert_eq!(
9521            compact_match.1, padded_match.1,
9522            "node paths must agree across whitespace; that's the whole point of NodePath",
9523        );
9524
9525        let mut frontend = FrontendState::new();
9526        frontend.program = padded_program;
9527
9528        // Stale source range from the compact source + correct node_path.
9529        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9530            compact_match.0,
9531            Some(compact_match.1),
9532            Number {
9533                value: 30.0,
9534                units: NumericSuffix::Mm,
9535            },
9536        )]);
9537
9538        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9539
9540        insta::assert_snapshot!(
9541            "test_commit_var_solution_survives_whitespace_shift_earlier_in_file",
9542            source_delta.text
9543        );
9544    }
9545
9546    /// When multiple `var` declarations exist and the stale source range
9547    /// happens to land on a *different* var, the node_path must take
9548    /// precedence and the right var gets updated.
9549    #[test]
9550    fn test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var() {
9551        let initial_source = "\
9552sketch(on = XY) {
9553  line1 = line(start = [var 10mm, var 0mm], end = [var 20mm, var 0mm])
9554}
9555";
9556        let program = Program::parse(initial_source).unwrap().0.unwrap();
9557
9558        let var_10 = collect_sketch_var_literals_with_value(&program, 10.0)
9559            .into_iter()
9560            .next()
9561            .expect("expected `var 10mm`");
9562        let var_20 = collect_sketch_var_literals_with_value(&program, 20.0)
9563            .into_iter()
9564            .next()
9565            .expect("expected `var 20mm`");
9566
9567        let mut frontend = FrontendState::new();
9568        frontend.program = program;
9569
9570        // Use var 20mm's source range, but var 10mm's node_path. node_path wins.
9571        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9572            var_20.0,
9573            Some(var_10.1),
9574            Number {
9575                value: 33.0,
9576                units: NumericSuffix::Mm,
9577            },
9578        )]);
9579
9580        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9581
9582        insta::assert_snapshot!(
9583            "test_commit_var_solution_node_path_wins_when_source_range_points_at_wrong_var",
9584            source_delta.text
9585        );
9586    }
9587
9588    /// Bare `var` (no initial literal) is only locatable via node_path. With
9589    /// the EditVarInitialValue handler now operating on the SketchVar node, a
9590    /// solver solution should fill the initial value in. The
9591    /// `@settings(experimentalFeatures = allow)` is required because bare `var`
9592    /// is gated as an experimental feature; without it the re-parse of the
9593    /// recast source rejects bare `var` declarations.
9594    #[test]
9595    fn test_commit_var_solution_writes_back_into_bare_var() {
9596        let initial_source = "\
9597@settings(experimentalFeatures = allow, kclVersion = 2.0)
9598sketch(on = XY) {
9599  line1 = line(start = [var, var 0mm], end = [var 10mm, var 0])
9600}
9601";
9602        let program = Program::parse(initial_source).unwrap().0.unwrap();
9603
9604        // Pick the first bare `var`; collect_all_sketch_vars returns every
9605        // SketchVar, including bare ones.
9606        let bare = collect_all_sketch_vars(&program)
9607            .into_iter()
9608            .find(|(range, _)| {
9609                // The bare `var` is exactly the 3 characters "var".
9610                range.end() - range.start() == 3
9611            })
9612            .expect("expected at least one bare `var`");
9613
9614        let mut frontend = FrontendState::new();
9615        frontend.program = program;
9616
9617        let outcome = empty_exec_outcome_with_var_solutions(vec![(
9618            bare.0,
9619            Some(bare.1),
9620            Number {
9621                value: 7.0,
9622                units: NumericSuffix::Mm,
9623            },
9624        )]);
9625
9626        let source_delta = frontend.commit_var_solutions_to_program(&outcome, "testing").unwrap();
9627
9628        // Default length unit (mm; no `@settings(defaultLengthUnit = …)`) is
9629        // written as an explicit suffix so the bare var commits with units.
9630        // The recast adds a blank line after the `@settings` annotation.
9631        insta::assert_snapshot!("test_commit_var_solution_writes_back_into_bare_var", source_delta.text);
9632    }
9633
9634    #[tokio::test(flavor = "multi_thread")]
9635    async fn test_delete_point_without_var() {
9636        let initial_source = "\
9637sketch(on = XY) {
9638  point(at = [var 1, var 2])
9639  point(at = [var 3, var 4])
9640  point(at = [var 5, var 6])
9641}
9642";
9643
9644        let program = Program::parse(initial_source).unwrap().0.unwrap();
9645
9646        let mut frontend = FrontendState::new();
9647
9648        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9649        let mock_ctx = ExecutorContext::new_mock(None).await;
9650        let version = Version(0);
9651
9652        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9653        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9654        let sketch_id = sketch_object.id;
9655        let sketch = expect_sketch(sketch_object);
9656
9657        let point_id = *sketch.segments.get(1).unwrap();
9658
9659        let (src_delta, scene_delta) = frontend
9660            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9661            .await
9662            .unwrap();
9663        insta::assert_snapshot!("test_delete_point_without_var", src_delta.text.as_str());
9664        assert_eq!(scene_delta.new_objects, vec![]);
9665        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9666
9667        ctx.close().await;
9668        mock_ctx.close().await;
9669    }
9670
9671    #[tokio::test(flavor = "multi_thread")]
9672    async fn test_delete_point_with_var() {
9673        let initial_source = "\
9674sketch(on = XY) {
9675  point(at = [var 1, var 2])
9676  point1 = point(at = [var 3, var 4])
9677  point(at = [var 5, var 6])
9678}
9679";
9680
9681        let program = Program::parse(initial_source).unwrap().0.unwrap();
9682
9683        let mut frontend = FrontendState::new();
9684
9685        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9686        let mock_ctx = ExecutorContext::new_mock(None).await;
9687        let version = Version(0);
9688
9689        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9690        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9691        let sketch_id = sketch_object.id;
9692        let sketch = expect_sketch(sketch_object);
9693
9694        let point_id = *sketch.segments.get(1).unwrap();
9695
9696        let (src_delta, scene_delta) = frontend
9697            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point_id])
9698            .await
9699            .unwrap();
9700        insta::assert_snapshot!("test_delete_point_with_var", src_delta.text.as_str());
9701        assert_eq!(scene_delta.new_objects, vec![]);
9702        assert_eq!(scene_delta.new_graph.objects.len(), 4);
9703
9704        ctx.close().await;
9705        mock_ctx.close().await;
9706    }
9707
9708    #[tokio::test(flavor = "multi_thread")]
9709    async fn test_delete_multiple_points() {
9710        let initial_source = "\
9711sketch(on = XY) {
9712  point(at = [var 1, var 2])
9713  point1 = point(at = [var 3, var 4])
9714  point(at = [var 5, var 6])
9715}
9716";
9717
9718        let program = Program::parse(initial_source).unwrap().0.unwrap();
9719
9720        let mut frontend = FrontendState::new();
9721
9722        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9723        let mock_ctx = ExecutorContext::new_mock(None).await;
9724        let version = Version(0);
9725
9726        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9727        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9728        let sketch_id = sketch_object.id;
9729
9730        let sketch = expect_sketch(sketch_object);
9731
9732        let point1_id = *sketch.segments.first().unwrap();
9733        let point2_id = *sketch.segments.get(1).unwrap();
9734
9735        let (src_delta, scene_delta) = frontend
9736            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point1_id, point2_id])
9737            .await
9738            .unwrap();
9739        insta::assert_snapshot!("test_delete_multiple_points", src_delta.text.as_str());
9740        assert_eq!(scene_delta.new_objects, vec![]);
9741        assert_eq!(scene_delta.new_graph.objects.len(), 3);
9742
9743        ctx.close().await;
9744        mock_ctx.close().await;
9745    }
9746
9747    #[tokio::test(flavor = "multi_thread")]
9748    async fn test_delete_coincident_constraint() {
9749        let initial_source = "\
9750sketch(on = XY) {
9751  point1 = point(at = [var 1, var 2])
9752  point2 = point(at = [var 3, var 4])
9753  coincident([point1, point2])
9754  point(at = [var 5, var 6])
9755}
9756";
9757
9758        let program = Program::parse(initial_source).unwrap().0.unwrap();
9759
9760        let mut frontend = FrontendState::new();
9761
9762        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9763        let mock_ctx = ExecutorContext::new_mock(None).await;
9764        let version = Version(0);
9765
9766        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9767        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9768        let sketch_id = sketch_object.id;
9769        let sketch = expect_sketch(sketch_object);
9770
9771        let coincident_id = *sketch.constraints.first().unwrap();
9772
9773        let (src_delta, scene_delta) = frontend
9774            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
9775            .await
9776            .unwrap();
9777        insta::assert_snapshot!("test_delete_coincident_constraint", src_delta.text.as_str());
9778        assert_eq!(scene_delta.new_objects, vec![]);
9779        assert_eq!(scene_delta.new_graph.objects.len(), 5);
9780
9781        ctx.close().await;
9782        mock_ctx.close().await;
9783    }
9784
9785    #[tokio::test(flavor = "multi_thread")]
9786    async fn test_delete_line_cascades_to_coincident_constraint() {
9787        let initial_source = "\
9788sketch(on = XY) {
9789  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9790  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9791  coincident([line1.end, line2.start])
9792}
9793";
9794
9795        let program = Program::parse(initial_source).unwrap().0.unwrap();
9796
9797        let mut frontend = FrontendState::new();
9798
9799        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9800        let mock_ctx = ExecutorContext::new_mock(None).await;
9801        let version = Version(0);
9802
9803        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9804        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9805        let sketch_id = sketch_object.id;
9806        let sketch = expect_sketch(sketch_object);
9807        let line_id = *sketch.segments.get(5).unwrap();
9808
9809        let (src_delta, scene_delta) = frontend
9810            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9811            .await
9812            .unwrap();
9813        insta::assert_snapshot!(
9814            "test_delete_line_cascades_to_coincident_constraint",
9815            src_delta.text.as_str()
9816        );
9817        assert_eq!(
9818            scene_delta.new_graph.objects.len(),
9819            5,
9820            "{:#?}",
9821            scene_delta.new_graph.objects
9822        );
9823
9824        ctx.close().await;
9825        mock_ctx.close().await;
9826    }
9827
9828    #[tokio::test(flavor = "multi_thread")]
9829    async fn test_delete_line_cascades_to_distance_constraint() {
9830        let initial_source = "\
9831sketch(on = XY) {
9832  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9833  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9834  distance([line1.end, line2.start]) == 10mm
9835}
9836";
9837
9838        let program = Program::parse(initial_source).unwrap().0.unwrap();
9839
9840        let mut frontend = FrontendState::new();
9841
9842        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
9843        let mock_ctx = ExecutorContext::new_mock(None).await;
9844        let version = Version(0);
9845
9846        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
9847        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9848        let sketch_id = sketch_object.id;
9849        let sketch = expect_sketch(sketch_object);
9850        let line_id = *sketch.segments.get(5).unwrap();
9851
9852        let (src_delta, scene_delta) = frontend
9853            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line_id])
9854            .await
9855            .unwrap();
9856        insta::assert_snapshot!(
9857            "test_delete_line_cascades_to_distance_constraint",
9858            src_delta.text.as_str()
9859        );
9860        assert_eq!(
9861            scene_delta.new_graph.objects.len(),
9862            5,
9863            "{:#?}",
9864            scene_delta.new_graph.objects
9865        );
9866
9867        ctx.close().await;
9868        mock_ctx.close().await;
9869    }
9870
9871    #[tokio::test(flavor = "multi_thread")]
9872    async fn test_delete_point_cascades_to_horizontal_distance_constraint() {
9873        let initial_source = "\
9874sketch(on = XY) {
9875  point1 = point(at = [var 1, var 2])
9876  point2 = point(at = [var 3, var 4])
9877  horizontalDistance([point1, point2]) == 10mm
9878}
9879";
9880
9881        let program = Program::parse(initial_source).unwrap().0.unwrap();
9882
9883        let mut frontend = FrontendState::new();
9884
9885        let mock_ctx = ExecutorContext::new_mock(None).await;
9886        let version = Version(0);
9887
9888        frontend.program = program.clone();
9889        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9890        frontend.update_state_after_exec(outcome, true);
9891        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9892        let sketch_id = sketch_object.id;
9893        let sketch = expect_sketch(sketch_object);
9894        let point2_id = *sketch.segments.get(1).unwrap();
9895
9896        let (src_delta, scene_delta) = frontend
9897            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point2_id])
9898            .await
9899            .unwrap();
9900        insta::assert_snapshot!(
9901            "test_delete_point_cascades_to_horizontal_distance_constraint",
9902            src_delta.text.as_str()
9903        );
9904        assert_eq!(
9905            scene_delta.new_graph.objects.len(),
9906            3,
9907            "{:#?}",
9908            scene_delta.new_graph.objects
9909        );
9910
9911        mock_ctx.close().await;
9912    }
9913
9914    #[tokio::test(flavor = "multi_thread")]
9915    async fn test_delete_line_cascades_to_fixed_constraint() {
9916        let initial_source = "\
9917sketch(on = XY) {
9918  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
9919  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
9920  fixed([line1.start, [0, 0]])
9921}
9922";
9923
9924        let program = Program::parse(initial_source).unwrap().0.unwrap();
9925
9926        let mut frontend = FrontendState::new();
9927
9928        let mock_ctx = ExecutorContext::new_mock(None).await;
9929        let version = Version(0);
9930
9931        frontend.program = program.clone();
9932        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9933        frontend.update_state_after_exec(outcome, true);
9934        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9935        let sketch_id = sketch_object.id;
9936        let sketch = expect_sketch(sketch_object);
9937        let line1_id = *sketch.segments.get(2).unwrap();
9938
9939        let (src_delta, scene_delta) = frontend
9940            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9941            .await
9942            .unwrap();
9943        insta::assert_snapshot!("test_delete_line_cascades_to_fixed_constraint", src_delta.text.as_str());
9944        assert_eq!(
9945            scene_delta.new_graph.objects.len(),
9946            5,
9947            "{:#?}",
9948            scene_delta.new_graph.objects
9949        );
9950
9951        mock_ctx.close().await;
9952    }
9953
9954    #[tokio::test(flavor = "multi_thread")]
9955    async fn test_delete_line_cascades_to_midpoint_constraint() {
9956        let initial_source = "\
9957sketch(on = XY) {
9958  point1 = point(at = [var 1, var 2])
9959  line1 = line(start = [var 0, var 0], end = [var 6, var 4])
9960  midpoint(line1, point = point1)
9961}
9962";
9963
9964        let program = Program::parse(initial_source).unwrap().0.unwrap();
9965
9966        let mut frontend = FrontendState::new();
9967
9968        let mock_ctx = ExecutorContext::new_mock(None).await;
9969        let version = Version(0);
9970
9971        frontend.program = program.clone();
9972        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
9973        frontend.update_state_after_exec(outcome, true);
9974        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
9975        let sketch_id = sketch_object.id;
9976        let sketch = expect_sketch(sketch_object);
9977        let line1_id = *sketch.segments.get(3).unwrap();
9978
9979        let (src_delta, scene_delta) = frontend
9980            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
9981            .await
9982            .unwrap();
9983        insta::assert_snapshot!(
9984            "test_delete_line_cascades_to_midpoint_constraint",
9985            src_delta.text.as_str()
9986        );
9987        assert_eq!(
9988            scene_delta.new_graph.objects.len(),
9989            3,
9990            "{:#?}",
9991            scene_delta.new_graph.objects
9992        );
9993
9994        mock_ctx.close().await;
9995    }
9996
9997    #[tokio::test(flavor = "multi_thread")]
9998    async fn test_delete_point_preserves_multiline_coincident_constraint() {
9999        let initial_source = "\
10000sketch(on = XY) {
10001  point1 = point(at = [var 1, var 2])
10002  point2 = point(at = [var 3, var 4])
10003  point3 = point(at = [var 5, var 6])
10004  coincident([point1, point2, point3])
10005}
10006";
10007
10008        let program = Program::parse(initial_source).unwrap().0.unwrap();
10009
10010        let mut frontend = FrontendState::new();
10011
10012        let mock_ctx = ExecutorContext::new_mock(None).await;
10013        let version = Version(0);
10014
10015        frontend.program = program.clone();
10016        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10017        frontend.update_state_after_exec(outcome, true);
10018        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10019        let sketch_id = sketch_object.id;
10020        let sketch = expect_sketch(sketch_object);
10021        let point3_id = *sketch.segments.get(2).unwrap();
10022
10023        let (src_delta, scene_delta) = frontend
10024            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![point3_id])
10025            .await
10026            .unwrap();
10027        assert!(src_delta.text.contains("point1 = point("), "{}", src_delta.text);
10028        assert!(src_delta.text.contains("point2 = point("), "{}", src_delta.text);
10029        assert!(!src_delta.text.contains("point3 = point("), "{}", src_delta.text);
10030        assert!(
10031            src_delta.text.contains("coincident([point1, point2])"),
10032            "{}",
10033            src_delta.text
10034        );
10035
10036        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10037        let sketch = expect_sketch(sketch_object);
10038        assert_eq!(sketch.segments.len(), 2);
10039        assert_eq!(sketch.constraints.len(), 1);
10040
10041        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10042        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10043            panic!("Expected constraint object");
10044        };
10045        let Constraint::Coincident(coincident) = constraint else {
10046            panic!("Expected coincident constraint");
10047        };
10048        assert_eq!(
10049            coincident.segments,
10050            sketch
10051                .segments
10052                .iter()
10053                .copied()
10054                .map(Into::into)
10055                .collect::<Vec<ConstraintSegment>>()
10056        );
10057
10058        mock_ctx.close().await;
10059    }
10060
10061    #[tokio::test(flavor = "multi_thread")]
10062    async fn test_delete_line_preserves_multiline_equal_length_constraint() {
10063        let initial_source = "\
10064sketch(on = XY) {
10065  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10066  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10067  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10068  equalLength([line1, line2, line3])
10069}
10070";
10071
10072        let program = Program::parse(initial_source).unwrap().0.unwrap();
10073
10074        let mut frontend = FrontendState::new();
10075
10076        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10077        let mock_ctx = ExecutorContext::new_mock(None).await;
10078        let version = Version(0);
10079
10080        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10081        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10082        let sketch_id = sketch_object.id;
10083        let sketch = expect_sketch(sketch_object);
10084        let line3_id = *sketch.segments.get(8).unwrap();
10085
10086        let (src_delta, scene_delta) = frontend
10087            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10088            .await
10089            .unwrap();
10090        insta::assert_snapshot!(
10091            "test_delete_line_preserves_multiline_equal_length_constraint",
10092            src_delta.text.as_str()
10093        );
10094
10095        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10096        let sketch = expect_sketch(sketch_object);
10097        assert_eq!(sketch.constraints.len(), 1);
10098
10099        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10100        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10101            panic!("Expected constraint object");
10102        };
10103        let Constraint::LinesEqualLength(lines_equal_length) = constraint else {
10104            panic!("Expected lines equal length constraint");
10105        };
10106        assert_eq!(lines_equal_length.lines.len(), 2);
10107
10108        ctx.close().await;
10109        mock_ctx.close().await;
10110    }
10111
10112    #[tokio::test(flavor = "multi_thread")]
10113    async fn test_delete_line_preserves_multiline_horizontal_constraint() {
10114        let initial_source = "\
10115sketch(on = XY) {
10116  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10117  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10118  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10119  horizontal([line1.end, line2.start, line3.start])
10120}
10121";
10122
10123        let program = Program::parse(initial_source).unwrap().0.unwrap();
10124
10125        let mut frontend = FrontendState::new();
10126
10127        let mock_ctx = ExecutorContext::new_mock(None).await;
10128        let version = Version(0);
10129
10130        frontend.program = program.clone();
10131        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10132        frontend.update_state_after_exec(outcome, true);
10133        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10134        let sketch_id = sketch_object.id;
10135        let sketch = expect_sketch(sketch_object);
10136        let line1_id = *sketch.segments.get(2).unwrap();
10137
10138        let (src_delta, scene_delta) = frontend
10139            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10140            .await
10141            .unwrap();
10142        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10143        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10144        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10145        assert!(
10146            src_delta.text.contains("horizontal([line2.start, line3.start])"),
10147            "{}",
10148            src_delta.text
10149        );
10150
10151        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10152        let sketch = expect_sketch(sketch_object);
10153        assert_eq!(sketch.constraints.len(), 1);
10154
10155        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10156        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10157            panic!("Expected constraint object");
10158        };
10159        let Constraint::Horizontal(Horizontal::Points { points }) = constraint else {
10160            panic!("Expected horizontal points constraint");
10161        };
10162        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10163        assert_eq!(*points, remaining_points);
10164
10165        mock_ctx.close().await;
10166    }
10167
10168    #[tokio::test(flavor = "multi_thread")]
10169    async fn test_delete_line_preserves_multiline_vertical_constraint() {
10170        let initial_source = "\
10171sketch(on = XY) {
10172  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10173  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10174  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10175  vertical([line1.end, line2.start, line3.start])
10176}
10177";
10178
10179        let program = Program::parse(initial_source).unwrap().0.unwrap();
10180
10181        let mut frontend = FrontendState::new();
10182
10183        let mock_ctx = ExecutorContext::new_mock(None).await;
10184        let version = Version(0);
10185
10186        frontend.program = program.clone();
10187        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10188        frontend.update_state_after_exec(outcome, true);
10189        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10190        let sketch_id = sketch_object.id;
10191        let sketch = expect_sketch(sketch_object);
10192        let line1_id = *sketch.segments.get(2).unwrap();
10193
10194        let (src_delta, scene_delta) = frontend
10195            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10196            .await
10197            .unwrap();
10198        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10199        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10200        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10201        assert!(
10202            src_delta.text.contains("vertical([line2.start, line3.start])"),
10203            "{}",
10204            src_delta.text
10205        );
10206
10207        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10208        let sketch = expect_sketch(sketch_object);
10209        assert_eq!(sketch.constraints.len(), 1);
10210
10211        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10212        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10213            panic!("Expected constraint object");
10214        };
10215        let Constraint::Vertical(Vertical::Points { points }) = constraint else {
10216            panic!("Expected vertical points constraint");
10217        };
10218        let remaining_points = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10219        assert_eq!(*points, remaining_points);
10220
10221        mock_ctx.close().await;
10222    }
10223
10224    #[tokio::test(flavor = "multi_thread")]
10225    async fn test_delete_line_preserves_multiline_coincident_constraint() {
10226        let initial_source = "\
10227sketch(on = XY) {
10228  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10229  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10230  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10231  coincident([line1.end, line2.start, line3.start])
10232}
10233";
10234
10235        let program = Program::parse(initial_source).unwrap().0.unwrap();
10236
10237        let mut frontend = FrontendState::new();
10238
10239        let mock_ctx = ExecutorContext::new_mock(None).await;
10240        let version = Version(0);
10241
10242        frontend.program = program.clone();
10243        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10244        frontend.update_state_after_exec(outcome, true);
10245        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10246        let sketch_id = sketch_object.id;
10247        let sketch = expect_sketch(sketch_object);
10248        let line1_id = *sketch.segments.get(2).unwrap();
10249
10250        let (src_delta, scene_delta) = frontend
10251            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line1_id])
10252            .await
10253            .unwrap();
10254        assert!(!src_delta.text.contains("line1 = line("), "{}", src_delta.text);
10255        assert!(src_delta.text.contains("line2 = line("), "{}", src_delta.text);
10256        assert!(src_delta.text.contains("line3 = line("), "{}", src_delta.text);
10257        assert!(
10258            src_delta.text.contains("coincident([line2.start, line3.start])"),
10259            "{}",
10260            src_delta.text
10261        );
10262
10263        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10264        let sketch = expect_sketch(sketch_object);
10265        assert_eq!(sketch.constraints.len(), 1);
10266
10267        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10268        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10269            panic!("Expected constraint object");
10270        };
10271        let Constraint::Coincident(coincident) = constraint else {
10272            panic!("Expected coincident constraint");
10273        };
10274        let remaining_segments = vec![sketch.segments[0].into(), sketch.segments[3].into()];
10275        assert_eq!(coincident.segments, remaining_segments);
10276
10277        mock_ctx.close().await;
10278    }
10279
10280    #[tokio::test(flavor = "multi_thread")]
10281    async fn test_delete_lines_removes_multiline_equal_length_constraint_below_minimum() {
10282        let initial_source = "\
10283sketch(on = XY) {
10284  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10285  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10286  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10287  equalLength([line1, line2, line3])
10288}
10289";
10290
10291        let program = Program::parse(initial_source).unwrap().0.unwrap();
10292
10293        let mut frontend = FrontendState::new();
10294
10295        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10296        let mock_ctx = ExecutorContext::new_mock(None).await;
10297        let version = Version(0);
10298
10299        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10300        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10301        let sketch_id = sketch_object.id;
10302        let sketch = expect_sketch(sketch_object);
10303        let line2_id = *sketch.segments.get(5).unwrap();
10304        let line3_id = *sketch.segments.get(8).unwrap();
10305
10306        let (src_delta, scene_delta) = frontend
10307            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10308            .await
10309            .unwrap();
10310        insta::assert_snapshot!(
10311            "test_delete_lines_removes_multiline_equal_length_constraint_below_minimum",
10312            src_delta.text.as_str()
10313        );
10314
10315        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10316        let sketch = expect_sketch(sketch_object);
10317        assert!(sketch.constraints.is_empty());
10318
10319        ctx.close().await;
10320        mock_ctx.close().await;
10321    }
10322
10323    #[tokio::test(flavor = "multi_thread")]
10324    async fn test_delete_line_preserves_multiline_parallel_constraint() {
10325        let initial_source = "\
10326sketch(on = XY) {
10327  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10328  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10329  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10330  parallel([line1, line2, line3])
10331}
10332";
10333
10334        let program = Program::parse(initial_source).unwrap().0.unwrap();
10335
10336        let mut frontend = FrontendState::new();
10337
10338        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10339        let mock_ctx = ExecutorContext::new_mock(None).await;
10340        let version = Version(0);
10341
10342        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10343        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10344        let sketch_id = sketch_object.id;
10345        let sketch = expect_sketch(sketch_object);
10346        let line3_id = *sketch.segments.get(8).unwrap();
10347
10348        let (src_delta, scene_delta) = frontend
10349            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line3_id])
10350            .await
10351            .unwrap();
10352        insta::assert_snapshot!(
10353            "test_delete_line_preserves_multiline_parallel_constraint",
10354            src_delta.text.as_str()
10355        );
10356
10357        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10358        let sketch = expect_sketch(sketch_object);
10359        assert_eq!(sketch.constraints.len(), 1);
10360
10361        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
10362        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10363            panic!("Expected constraint object");
10364        };
10365        let Constraint::Parallel(parallel) = constraint else {
10366            panic!("Expected parallel constraint");
10367        };
10368        assert_eq!(parallel.lines.len(), 2);
10369
10370        ctx.close().await;
10371        mock_ctx.close().await;
10372    }
10373
10374    #[tokio::test(flavor = "multi_thread")]
10375    async fn test_delete_lines_removes_multiline_parallel_constraint_below_minimum() {
10376        let initial_source = "\
10377sketch(on = XY) {
10378  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10379  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10380  line3 = line(start = [var 9, var 10], end = [var 11, var 12])
10381  parallel([line1, line2, line3])
10382}
10383";
10384
10385        let program = Program::parse(initial_source).unwrap().0.unwrap();
10386
10387        let mut frontend = FrontendState::new();
10388
10389        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10390        let mock_ctx = ExecutorContext::new_mock(None).await;
10391        let version = Version(0);
10392
10393        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10394        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10395        let sketch_id = sketch_object.id;
10396        let sketch = expect_sketch(sketch_object);
10397        let line2_id = *sketch.segments.get(5).unwrap();
10398        let line3_id = *sketch.segments.get(8).unwrap();
10399
10400        let (src_delta, scene_delta) = frontend
10401            .delete_objects(&mock_ctx, version, sketch_id, Vec::new(), vec![line2_id, line3_id])
10402            .await
10403            .unwrap();
10404        insta::assert_snapshot!(
10405            "test_delete_lines_removes_multiline_parallel_constraint_below_minimum",
10406            src_delta.text.as_str()
10407        );
10408
10409        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
10410        let sketch = expect_sketch(sketch_object);
10411        assert!(sketch.constraints.is_empty());
10412
10413        ctx.close().await;
10414        mock_ctx.close().await;
10415    }
10416
10417    #[tokio::test(flavor = "multi_thread")]
10418    async fn test_delete_line_line_coincident_constraint() {
10419        let initial_source = "\
10420sketch(on = XY) {
10421  line1 = line(start = [var 1, var 2], end = [var 3, var 4])
10422  line2 = line(start = [var 5, var 6], end = [var 7, var 8])
10423  coincident([line1, line2])
10424}
10425";
10426
10427        let program = Program::parse(initial_source).unwrap().0.unwrap();
10428
10429        let mut frontend = FrontendState::new();
10430
10431        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10432        let mock_ctx = ExecutorContext::new_mock(None).await;
10433        let version = Version(0);
10434
10435        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
10436        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10437        let sketch_id = sketch_object.id;
10438        let sketch = expect_sketch(sketch_object);
10439
10440        let coincident_id = *sketch.constraints.first().unwrap();
10441
10442        let (src_delta, scene_delta) = frontend
10443            .delete_objects(&mock_ctx, version, sketch_id, vec![coincident_id], Vec::new())
10444            .await
10445            .unwrap();
10446        insta::assert_snapshot!("test_delete_line_line_coincident_constraint", src_delta.text.as_str());
10447        assert_eq!(scene_delta.new_objects, vec![]);
10448        assert_eq!(scene_delta.new_graph.objects.len(), 8);
10449
10450        ctx.close().await;
10451        mock_ctx.close().await;
10452    }
10453
10454    #[tokio::test(flavor = "multi_thread")]
10455    async fn test_two_points_coincident() {
10456        let initial_source = "\
10457sketch(on = XY) {
10458  point1 = point(at = [var 1, var 2])
10459  point(at = [3, 4])
10460}
10461";
10462
10463        let program = Program::parse(initial_source).unwrap().0.unwrap();
10464
10465        let mut frontend = FrontendState::new();
10466
10467        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10468        let mock_ctx = ExecutorContext::new_mock(None).await;
10469        let version = Version(0);
10470
10471        frontend.hack_set_program(&ctx, program).await.unwrap();
10472        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10473        let sketch_id = sketch_object.id;
10474        let sketch = expect_sketch(sketch_object);
10475        let point0_id = *sketch.segments.first().unwrap();
10476        let point1_id = *sketch.segments.get(1).unwrap();
10477
10478        let constraint = Constraint::Coincident(Coincident {
10479            segments: vec![point0_id.into(), point1_id.into()],
10480        });
10481        let (src_delta, scene_delta) = frontend
10482            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10483            .await
10484            .unwrap();
10485        insta::assert_snapshot!("test_two_points_coincident", src_delta.text.as_str());
10486        assert_eq!(
10487            scene_delta.new_graph.objects.len(),
10488            5,
10489            "{:#?}",
10490            scene_delta.new_graph.objects
10491        );
10492
10493        ctx.close().await;
10494        mock_ctx.close().await;
10495    }
10496
10497    #[tokio::test(flavor = "multi_thread")]
10498    async fn test_three_points_coincident() {
10499        let initial_source = "\
10500sketch(on = XY) {
10501  point1 = point(at = [var 1, var 2])
10502  point(at = [var 3, var 4])
10503  point(at = [var 5, var 6])
10504}
10505";
10506
10507        let program = Program::parse(initial_source).unwrap().0.unwrap();
10508
10509        let mut frontend = FrontendState::new();
10510
10511        let mock_ctx = ExecutorContext::new_mock(None).await;
10512        let version = Version(0);
10513
10514        frontend.program = program.clone();
10515        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10516        frontend.update_state_after_exec(outcome, true);
10517        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10518        let sketch_id = sketch_object.id;
10519        let sketch = expect_sketch(sketch_object);
10520        let segments = sketch
10521            .segments
10522            .iter()
10523            .take(3)
10524            .copied()
10525            .map(Into::into)
10526            .collect::<Vec<ConstraintSegment>>();
10527
10528        let constraint = Constraint::Coincident(Coincident {
10529            segments: segments.clone(),
10530        });
10531        let (src_delta, scene_delta) = frontend
10532            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10533            .await
10534            .unwrap();
10535        insta::assert_snapshot!("test_three_points_coincident", src_delta.text.as_str());
10536
10537        let constraint_object = scene_delta
10538            .new_graph
10539            .objects
10540            .iter()
10541            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10542            .unwrap();
10543
10544        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10545            panic!("expected a constraint object");
10546        };
10547
10548        assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10549
10550        mock_ctx.close().await;
10551    }
10552
10553    #[tokio::test(flavor = "multi_thread")]
10554    async fn test_source_with_three_point_coincident_tracks_all_segments() {
10555        let initial_source = "\
10556sketch(on = XY) {
10557  point1 = point(at = [var 1, var 2])
10558  point2 = point(at = [var 3, var 4])
10559  point3 = point(at = [var 5, var 6])
10560  coincident([point1, point2, point3])
10561}
10562";
10563
10564        let program = Program::parse(initial_source).unwrap().0.unwrap();
10565
10566        let mut frontend = FrontendState::new();
10567
10568        let ctx = ExecutorContext::new_mock(None).await;
10569        frontend.program = program.clone();
10570        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10571        frontend.update_state_after_exec(outcome, true);
10572
10573        let constraint_object = frontend
10574            .scene_graph
10575            .objects
10576            .iter()
10577            .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10578            .unwrap();
10579        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10580            panic!("expected a constraint object");
10581        };
10582
10583        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10584        let sketch = expect_sketch(sketch_object);
10585        let expected_segments = sketch
10586            .segments
10587            .iter()
10588            .take(3)
10589            .copied()
10590            .map(Into::into)
10591            .collect::<Vec<ConstraintSegment>>();
10592
10593        assert_eq!(
10594            constraint,
10595            &Constraint::Coincident(Coincident {
10596                segments: expected_segments,
10597            })
10598        );
10599
10600        ctx.close().await;
10601    }
10602
10603    #[tokio::test(flavor = "multi_thread")]
10604    async fn test_point_origin_coincident_preserves_order() {
10605        let initial_source = "\
10606sketch(on = XY) {
10607  point(at = [var 1, var 2])
10608}
10609";
10610
10611        for (origin_first, snapshot_name) in [
10612            (true, "test_point_origin_coincident_preserves_order_origin_first"),
10613            (false, "test_point_origin_coincident_preserves_order_point_first"),
10614        ] {
10615            let program = Program::parse(initial_source).unwrap().0.unwrap();
10616
10617            let mut frontend = FrontendState::new();
10618
10619            let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10620            let mock_ctx = ExecutorContext::new_mock(None).await;
10621            let version = Version(0);
10622
10623            frontend.hack_set_program(&ctx, program).await.unwrap();
10624            let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10625            let sketch_id = sketch_object.id;
10626            let sketch = expect_sketch(sketch_object);
10627            let point_id = *sketch.segments.first().unwrap();
10628
10629            let segments = if origin_first {
10630                vec![ConstraintSegment::ORIGIN, point_id.into()]
10631            } else {
10632                vec![point_id.into(), ConstraintSegment::ORIGIN]
10633            };
10634            let constraint = Constraint::Coincident(Coincident {
10635                segments: segments.clone(),
10636            });
10637            let (src_delta, scene_delta) = frontend
10638                .add_constraint(&mock_ctx, version, sketch_id, constraint)
10639                .await
10640                .unwrap();
10641            insta::assert_snapshot!(snapshot_name, src_delta.text.as_str());
10642
10643            let constraint_object = scene_delta
10644                .new_graph
10645                .objects
10646                .iter()
10647                .find(|obj| matches!(obj.kind, ObjectKind::Constraint { .. }))
10648                .unwrap();
10649
10650            let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
10651                panic!("expected a constraint object");
10652            };
10653
10654            assert_eq!(constraint, &Constraint::Coincident(Coincident { segments }));
10655
10656            ctx.close().await;
10657            mock_ctx.close().await;
10658        }
10659    }
10660
10661    #[tokio::test(flavor = "multi_thread")]
10662    async fn test_coincident_of_line_end_points() {
10663        let initial_source = "\
10664sketch(on = XY) {
10665  line(start = [var 1, var 2], end = [var 3, var 4])
10666  line(start = [var 5, var 6], end = [var 7, var 8])
10667}
10668";
10669
10670        let program = Program::parse(initial_source).unwrap().0.unwrap();
10671
10672        let mut frontend = FrontendState::new();
10673
10674        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10675        let mock_ctx = ExecutorContext::new_mock(None).await;
10676        let version = Version(0);
10677
10678        frontend.hack_set_program(&ctx, program).await.unwrap();
10679        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10680        let sketch_id = sketch_object.id;
10681        let sketch = expect_sketch(sketch_object);
10682        let point0_id = *sketch.segments.get(1).unwrap();
10683        let point1_id = *sketch.segments.get(3).unwrap();
10684
10685        let constraint = Constraint::Coincident(Coincident {
10686            segments: vec![point0_id.into(), point1_id.into()],
10687        });
10688        let (src_delta, scene_delta) = frontend
10689            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10690            .await
10691            .unwrap();
10692        insta::assert_snapshot!("test_coincident_of_line_end_points", src_delta.text.as_str());
10693        assert_eq!(
10694            scene_delta.new_graph.objects.len(),
10695            9,
10696            "{:#?}",
10697            scene_delta.new_graph.objects
10698        );
10699
10700        ctx.close().await;
10701        mock_ctx.close().await;
10702    }
10703
10704    #[tokio::test(flavor = "multi_thread")]
10705    async fn test_coincident_of_line_point_and_circle_segment() {
10706        let initial_source = "\
10707sketch(on = XY) {
10708  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
10709  line1 = line(start = [var 9mm, var 1mm], end = [var 10mm, var 2mm])
10710}
10711";
10712        let program = Program::parse(initial_source).unwrap().0.unwrap();
10713        let mut frontend = FrontendState::new();
10714
10715        let mock_ctx = ExecutorContext::new_mock(None).await;
10716        let version = Version(0);
10717
10718        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10719        frontend.program = program;
10720        frontend.update_state_after_exec(outcome, true);
10721        let sketch_object = find_first_sketch_object(&frontend.scene_graph).expect("Expected sketch object");
10722        let sketch_id = sketch_object.id;
10723        let sketch = expect_sketch(sketch_object);
10724
10725        let circle_id = sketch
10726            .segments
10727            .iter()
10728            .copied()
10729            .find(|seg_id| {
10730                matches!(
10731                    &frontend.scene_graph.objects[seg_id.0].kind,
10732                    ObjectKind::Segment {
10733                        segment: Segment::Circle(_)
10734                    }
10735                )
10736            })
10737            .expect("Expected a circle segment in sketch");
10738        let line_id = frontend
10739            .scene_graph
10740            .objects
10741            .iter()
10742            .find_map(|obj| match &obj.kind {
10743                ObjectKind::Segment {
10744                    segment: Segment::Line(line),
10745                } if line.owner.is_none() => Some(obj.id),
10746                _ => None,
10747            })
10748            .expect("Expected a standalone line segment in scene graph");
10749
10750        let line_start_point_id = match &frontend.scene_graph.objects[line_id.0].kind {
10751            ObjectKind::Segment {
10752                segment: Segment::Line(line),
10753            } => line.start,
10754            _ => panic!("Expected line segment object"),
10755        };
10756
10757        let constraint = Constraint::Coincident(Coincident {
10758            segments: vec![line_start_point_id.into(), circle_id.into()],
10759        });
10760        let (src_delta, _scene_delta) = frontend
10761            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10762            .await
10763            .unwrap();
10764        insta::assert_snapshot!(
10765            "test_coincident_of_line_point_and_circle_segment",
10766            src_delta.text.as_str()
10767        );
10768
10769        mock_ctx.close().await;
10770    }
10771
10772    #[tokio::test(flavor = "multi_thread")]
10773    async fn test_invalid_coincident_arc_and_line_preserves_state() {
10774        // Test that attempting an invalid coincident constraint (arc and line)
10775        // doesn't corrupt the state, allowing subsequent operations to work.
10776        // This test verifies the transactional fix in add_constraint that prevents
10777        // state corruption when invalid constraints are attempted.
10778        // Example: coincident constraint between an arc segment and a straight line segment
10779        // is geometrically invalid and should fail, but state should remain intact.
10780        // Use the programmatic approach (new_sketch + add_segment) like test_new_sketch_add_arc_edit_arc
10781        let program = Program::empty();
10782
10783        let mut frontend = FrontendState::new();
10784        frontend.program = program;
10785
10786        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10787        let mock_ctx = ExecutorContext::new_mock(None).await;
10788        let version = Version(0);
10789
10790        let sketch_args = SketchCtor {
10791            on: Plane::Default(PlaneName::Xy),
10792        };
10793        let (_src_delta, _scene_delta, sketch_id) = frontend
10794            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
10795            .await
10796            .unwrap();
10797
10798        // Add an arc segment
10799        let arc_ctor = ArcCtor {
10800            start: Point2d {
10801                x: Expr::Var(Number {
10802                    value: 0.0,
10803                    units: NumericSuffix::Mm,
10804                }),
10805                y: Expr::Var(Number {
10806                    value: 0.0,
10807                    units: NumericSuffix::Mm,
10808                }),
10809            },
10810            end: Point2d {
10811                x: Expr::Var(Number {
10812                    value: 10.0,
10813                    units: NumericSuffix::Mm,
10814                }),
10815                y: Expr::Var(Number {
10816                    value: 10.0,
10817                    units: NumericSuffix::Mm,
10818                }),
10819            },
10820            center: Point2d {
10821                x: Expr::Var(Number {
10822                    value: 10.0,
10823                    units: NumericSuffix::Mm,
10824                }),
10825                y: Expr::Var(Number {
10826                    value: 0.0,
10827                    units: NumericSuffix::Mm,
10828                }),
10829            },
10830            direction: None,
10831            construction: None,
10832        };
10833        let (_src_delta, scene_delta) = frontend
10834            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
10835            .await
10836            .unwrap();
10837        // The arc is the last object in new_objects (after the 3 points: start, end, center)
10838        let arc_id = *scene_delta.new_objects.last().unwrap();
10839
10840        // Add a line segment
10841        let line_ctor = LineCtor {
10842            start: Point2d {
10843                x: Expr::Var(Number {
10844                    value: 20.0,
10845                    units: NumericSuffix::Mm,
10846                }),
10847                y: Expr::Var(Number {
10848                    value: 0.0,
10849                    units: NumericSuffix::Mm,
10850                }),
10851            },
10852            end: Point2d {
10853                x: Expr::Var(Number {
10854                    value: 30.0,
10855                    units: NumericSuffix::Mm,
10856                }),
10857                y: Expr::Var(Number {
10858                    value: 10.0,
10859                    units: NumericSuffix::Mm,
10860                }),
10861            },
10862            construction: None,
10863        };
10864        let (_src_delta, scene_delta) = frontend
10865            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Line(line_ctor), None)
10866            .await
10867            .unwrap();
10868        // The line is the last object in new_objects (after the 2 points: start, end)
10869        let line_id = *scene_delta.new_objects.last().unwrap();
10870
10871        // Attempt to add an invalid coincident constraint between arc and line
10872        // This should fail during execution, but state should remain intact
10873        let constraint = Constraint::Coincident(Coincident {
10874            segments: vec![arc_id.into(), line_id.into()],
10875        });
10876        let result = frontend.add_constraint(&mock_ctx, version, sketch_id, constraint).await;
10877
10878        // The constraint addition should fail (invalid constraint)
10879        assert!(result.is_err(), "Expected invalid coincident constraint to fail");
10880
10881        // Verify state is not corrupted by checking that we can still access the scene graph
10882        // and that the original segments are still present with their source ranges
10883        let sketch_object_after =
10884            find_first_sketch_object(&frontend.scene_graph).expect("Sketch should still exist after failed constraint");
10885        let sketch_after = expect_sketch(sketch_object_after);
10886
10887        // Verify both segments are still in the sketch
10888        assert!(
10889            sketch_after.segments.contains(&arc_id),
10890            "Arc segment should still exist after failed constraint"
10891        );
10892        assert!(
10893            sketch_after.segments.contains(&line_id),
10894            "Line segment should still exist after failed constraint"
10895        );
10896
10897        // Verify we can still access segment objects (this would fail if source ranges were corrupted)
10898        let arc_obj = frontend
10899            .scene_graph
10900            .objects
10901            .get(arc_id.0)
10902            .expect("Arc object should still be accessible");
10903        let line_obj = frontend
10904            .scene_graph
10905            .objects
10906            .get(line_id.0)
10907            .expect("Line object should still be accessible");
10908
10909        // Verify source ranges are still valid (not corrupted)
10910        // Just verify that the objects are still accessible and have the expected types
10911        match &arc_obj.kind {
10912            ObjectKind::Segment {
10913                segment: Segment::Arc(_),
10914            } => {}
10915            _ => panic!("Arc object should still be an arc segment"),
10916        }
10917        match &line_obj.kind {
10918            ObjectKind::Segment {
10919                segment: Segment::Line(_),
10920            } => {}
10921            _ => panic!("Line object should still be a line segment"),
10922        }
10923
10924        ctx.close().await;
10925        mock_ctx.close().await;
10926    }
10927
10928    #[tokio::test(flavor = "multi_thread")]
10929    async fn test_distance_two_points() {
10930        let initial_source = "\
10931sketch(on = XY) {
10932  point(at = [var 1, var 2])
10933  point(at = [var 3, var 4])
10934}
10935";
10936
10937        let program = Program::parse(initial_source).unwrap().0.unwrap();
10938
10939        let mut frontend = FrontendState::new();
10940
10941        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
10942        let mock_ctx = ExecutorContext::new_mock(None).await;
10943        let version = Version(0);
10944
10945        frontend.hack_set_program(&ctx, program).await.unwrap();
10946        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10947        let sketch_id = sketch_object.id;
10948        let sketch = expect_sketch(sketch_object);
10949        let point0_id = *sketch.segments.first().unwrap();
10950        let point1_id = *sketch.segments.get(1).unwrap();
10951
10952        let constraint = Constraint::Distance(Distance {
10953            segments: vec![point0_id.into(), point1_id.into()],
10954            distance: Number {
10955                value: 2.0,
10956                units: NumericSuffix::Mm,
10957            },
10958            label_position: None,
10959            source: Default::default(),
10960        });
10961        let (src_delta, scene_delta) = frontend
10962            .add_constraint(&mock_ctx, version, sketch_id, constraint)
10963            .await
10964            .unwrap();
10965        insta::assert_snapshot!("test_distance_two_points", src_delta.text.as_str());
10966        assert_eq!(
10967            scene_delta.new_graph.objects.len(),
10968            5,
10969            "{:#?}",
10970            scene_delta.new_graph.objects
10971        );
10972
10973        ctx.close().await;
10974        mock_ctx.close().await;
10975    }
10976
10977    #[tokio::test(flavor = "multi_thread")]
10978    async fn test_distance_two_points_with_label() {
10979        let initial_source = "\
10980sketch(on = XY) {
10981  point(at = [var 1, var 2])
10982  point(at = [var 3, var 4])
10983}
10984";
10985
10986        let program = Program::parse(initial_source).unwrap().0.unwrap();
10987
10988        let mut frontend = FrontendState::new();
10989
10990        let mock_ctx = ExecutorContext::new_mock(None).await;
10991        let version = Version(0);
10992
10993        frontend.program = program.clone();
10994        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
10995        frontend.update_state_after_exec(outcome, true);
10996        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
10997        let sketch_id = sketch_object.id;
10998        let sketch = expect_sketch(sketch_object);
10999        let point0_id = *sketch.segments.first().unwrap();
11000        let point1_id = *sketch.segments.get(1).unwrap();
11001
11002        let label_position = Point2d {
11003            x: Number {
11004                value: 10.0,
11005                units: NumericSuffix::Mm,
11006            },
11007            y: Number {
11008                value: 11.0,
11009                units: NumericSuffix::Mm,
11010            },
11011        };
11012        let constraint = Constraint::Distance(Distance {
11013            segments: vec![point0_id.into(), point1_id.into()],
11014            distance: Number {
11015                value: 2.0,
11016                units: NumericSuffix::Mm,
11017            },
11018            label_position: Some(label_position.clone()),
11019            source: Default::default(),
11020        });
11021        let (src_delta, scene_delta) = frontend
11022            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11023            .await
11024            .unwrap();
11025        insta::assert_snapshot!("test_distance_two_points_with_label", src_delta.text.as_str());
11026
11027        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11028        let sketch = expect_sketch(sketch_object);
11029        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11030        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11031            panic!("Expected constraint object");
11032        };
11033        let Constraint::Distance(distance) = constraint else {
11034            panic!("Expected distance constraint");
11035        };
11036        assert_eq!(distance.label_position, Some(label_position));
11037
11038        mock_ctx.close().await;
11039    }
11040
11041    #[tokio::test(flavor = "multi_thread")]
11042    async fn test_edit_distance_constraint_label_position() {
11043        let initial_source = "\
11044sketch(on = XY) {
11045  point(at = [var 1, var 2])
11046  point(at = [var 3, var 2])
11047}
11048";
11049
11050        let program = Program::parse(initial_source).unwrap().0.unwrap();
11051
11052        let mut frontend = FrontendState::new();
11053
11054        let mock_ctx = ExecutorContext::new_mock(None).await;
11055        let version = Version(0);
11056
11057        frontend.program = program.clone();
11058        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11059        frontend.update_state_after_exec(outcome, true);
11060        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11061        let sketch_id = sketch_object.id;
11062        let sketch = expect_sketch(sketch_object);
11063        let point0_id = *sketch.segments.first().unwrap();
11064        let point1_id = *sketch.segments.get(1).unwrap();
11065
11066        let constraint = Constraint::Distance(Distance {
11067            segments: vec![point0_id.into(), point1_id.into()],
11068            distance: Number {
11069                value: 2.0,
11070                units: NumericSuffix::Mm,
11071            },
11072            label_position: None,
11073            source: Default::default(),
11074        });
11075        let (_, scene_delta) = frontend
11076            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11077            .await
11078            .unwrap();
11079        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11080        let sketch = expect_sketch(sketch_object);
11081        let constraint_id = sketch.constraints[0];
11082        let label_position = Point2d {
11083            x: Number {
11084                value: 10.0,
11085                units: NumericSuffix::Mm,
11086            },
11087            y: Number {
11088                value: 11.0,
11089                units: NumericSuffix::Mm,
11090            },
11091        };
11092
11093        let (src_delta, scene_delta) = frontend
11094            .edit_distance_constraint_label_position(
11095                &mock_ctx,
11096                version,
11097                sketch_id,
11098                constraint_id,
11099                label_position.clone(),
11100                vec![],
11101            )
11102            .await
11103            .unwrap();
11104        insta::assert_snapshot!("test_edit_distance_constraint_label_position", src_delta.text.as_str());
11105
11106        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11107        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11108            panic!("Expected constraint object");
11109        };
11110        let Constraint::Distance(distance) = constraint else {
11111            panic!("Expected distance constraint");
11112        };
11113        assert_eq!(distance.label_position, Some(label_position));
11114
11115        mock_ctx.close().await;
11116    }
11117
11118    #[tokio::test(flavor = "multi_thread")]
11119    async fn test_edit_distance_constraint_type_and_value() {
11120        let initial_source = "\
11121sketch(on = XY) {
11122  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11123  distance([line1.start, line1.end]) == 5mm
11124}
11125";
11126
11127        let program = Program::parse(initial_source).unwrap().0.unwrap();
11128        let mut frontend = FrontendState::new();
11129        let mock_ctx = ExecutorContext::new_mock(None).await;
11130        let version = Version(0);
11131
11132        frontend.program = program.clone();
11133        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11134        frontend.update_state_after_exec(outcome, true);
11135        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11136        let sketch_id = sketch_object.id;
11137        let sketch = expect_sketch(sketch_object);
11138        let constraint_id = sketch.constraints[0];
11139        let point0_id = sketch.segments[0];
11140        let point1_id = sketch.segments[1];
11141        let label_position = Point2d {
11142            x: Number {
11143                value: 2.0,
11144                units: NumericSuffix::Mm,
11145            },
11146            y: Number {
11147                value: 5.0,
11148                units: NumericSuffix::Mm,
11149            },
11150        };
11151
11152        let (source_delta, scene_delta) = frontend
11153            .edit_distance_constraint_with_options(
11154                &mock_ctx,
11155                version,
11156                sketch_id,
11157                constraint_id,
11158                Constraint::HorizontalDistance(Distance {
11159                    segments: vec![point0_id.into(), point1_id.into()],
11160                    distance: Number {
11161                        value: 4.0,
11162                        units: NumericSuffix::Mm,
11163                    },
11164                    label_position: Some(label_position.clone()),
11165                    source: Default::default(),
11166                }),
11167                EditConstraintOptions {
11168                    commit_solved_initial_guesses: false,
11169                },
11170            )
11171            .await
11172            .unwrap();
11173        assert_eq!(
11174            source_delta.text,
11175            "\
11176sketch(on = XY) {
11177  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 3mm])
11178  horizontalDistance([line1.start, line1.end], labelPosition = [2mm, 5mm]) == 4mm
11179}
11180"
11181        );
11182
11183        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11184        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11185            panic!("Expected constraint object");
11186        };
11187        let Constraint::HorizontalDistance(distance) = constraint else {
11188            panic!("Expected horizontal distance constraint");
11189        };
11190        assert_eq!(distance.distance.value, 4.0);
11191        assert_eq!(distance.label_position, Some(label_position));
11192
11193        mock_ctx.close().await;
11194    }
11195
11196    #[tokio::test(flavor = "multi_thread")]
11197    async fn test_edit_angle_constraint_label_position() {
11198        let initial_source = "\
11199sketch(on = XY) {
11200  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11201  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11202  angle([line1, line2]) == 60deg
11203}
11204";
11205
11206        let program = Program::parse(initial_source).unwrap().0.unwrap();
11207        let mut frontend = FrontendState::new();
11208        let mock_ctx = ExecutorContext::new_mock(None).await;
11209        let version = Version(0);
11210
11211        frontend.program = program.clone();
11212        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11213        frontend.update_state_after_exec(outcome, true);
11214        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11215        let sketch_id = sketch_object.id;
11216        let sketch = expect_sketch(sketch_object);
11217        let constraint_id = sketch.constraints[0];
11218        let label_position = Point2d {
11219            x: Number {
11220                value: 10.0,
11221                units: NumericSuffix::Mm,
11222            },
11223            y: Number {
11224                value: 11.0,
11225                units: NumericSuffix::Mm,
11226            },
11227        };
11228
11229        let (src_delta, scene_delta) = frontend
11230            .edit_distance_constraint_label_position(
11231                &mock_ctx,
11232                version,
11233                sketch_id,
11234                constraint_id,
11235                label_position.clone(),
11236                vec![],
11237            )
11238            .await
11239            .unwrap();
11240        assert_eq!(
11241            src_delta.text.as_str(),
11242            "\
11243sketch(on = XY) {
11244  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11245  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11246  angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
11247}
11248"
11249        );
11250
11251        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11252        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11253            panic!("Expected constraint object");
11254        };
11255        let Constraint::Angle(angle) = constraint else {
11256            panic!("Expected angle constraint");
11257        };
11258        assert_eq!(angle.label_position, Some(label_position));
11259
11260        mock_ctx.close().await;
11261    }
11262
11263    #[tokio::test(flavor = "multi_thread")]
11264    async fn test_edit_angle_constraint_label_position_with_call_on_right() {
11265        let initial_source = "\
11266sketch(on = XY) {
11267  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11268  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11269  60deg == angleDimension(lines = [line1, line2], sector = 1)
11270}
11271";
11272
11273        let program = Program::parse(initial_source).unwrap().0.unwrap();
11274        let mut frontend = FrontendState::new();
11275        let mock_ctx = ExecutorContext::new_mock(None).await;
11276        let version = Version(0);
11277
11278        frontend.program = program.clone();
11279        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11280        frontend.update_state_after_exec(outcome, true);
11281        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11282        let sketch_id = sketch_object.id;
11283        let sketch = expect_sketch(sketch_object);
11284        let constraint_id = sketch.constraints[0];
11285        let label_position = Point2d {
11286            x: Number {
11287                value: 10.0,
11288                units: NumericSuffix::Mm,
11289            },
11290            y: Number {
11291                value: 11.0,
11292                units: NumericSuffix::Mm,
11293            },
11294        };
11295
11296        let (src_delta, scene_delta) = frontend
11297            .edit_distance_constraint_label_position(
11298                &mock_ctx,
11299                version,
11300                sketch_id,
11301                constraint_id,
11302                label_position.clone(),
11303                vec![],
11304            )
11305            .await
11306            .unwrap();
11307        assert_eq!(
11308            src_delta.text.as_str(),
11309            "\
11310sketch(on = XY) {
11311  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11312  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.46mm])
11313  60deg == angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm])
11314}
11315"
11316        );
11317
11318        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11319        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11320            panic!("Expected constraint object");
11321        };
11322        let Constraint::Angle(angle) = constraint else {
11323            panic!("Expected angle constraint");
11324        };
11325        assert_eq!(angle.label_position, Some(label_position));
11326
11327        mock_ctx.close().await;
11328    }
11329
11330    #[tokio::test(flavor = "multi_thread")]
11331    async fn test_edit_angle_constraint() {
11332        let initial_source = "\
11333sketch(on = XY) {
11334  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11335  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11336  angle([line1, line2]) == 60deg
11337}
11338";
11339
11340        let program = Program::parse(initial_source).unwrap().0.unwrap();
11341        let mut frontend = FrontendState::new();
11342        let mock_ctx = ExecutorContext::new_mock(None).await;
11343        let version = Version(0);
11344
11345        frontend.program = program.clone();
11346        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11347        frontend.update_state_after_exec(outcome, true);
11348        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11349        let sketch_id = sketch_object.id;
11350        let sketch = expect_sketch(sketch_object);
11351        let constraint_id = sketch.constraints[0];
11352        let line1_id = *sketch.segments.get(2).unwrap();
11353        let line2_id = *sketch.segments.get(5).unwrap();
11354        let label_position = Point2d {
11355            x: Number {
11356                value: 10.0,
11357                units: NumericSuffix::Mm,
11358            },
11359            y: Number {
11360                value: 11.0,
11361                units: NumericSuffix::Mm,
11362            },
11363        };
11364
11365        let (src_delta, scene_delta) = frontend
11366            .edit_angle_constraint_with_options(
11367                &mock_ctx,
11368                version,
11369                sketch_id,
11370                constraint_id,
11371                Angle {
11372                    lines: vec![line2_id, line1_id],
11373                    angle: Number {
11374                        value: 60.0,
11375                        units: NumericSuffix::Deg,
11376                    },
11377                    sector: Some(3),
11378                    inverse: Some(false),
11379                    label_position: Some(label_position.clone()),
11380                    source: Default::default(),
11381                },
11382                EditConstraintOptions {
11383                    commit_solved_initial_guesses: false,
11384                },
11385            )
11386            .await
11387            .unwrap();
11388        assert_eq!(
11389            src_delta.text.as_str(),
11390            "\
11391sketch(on = XY) {
11392  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11393  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11394  angleDimension(lines = [line2, line1], sector = 3, labelPosition = [10mm, 11mm]) == 60deg
11395}
11396"
11397        );
11398
11399        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
11400        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11401            panic!("Expected constraint object");
11402        };
11403        let Constraint::Angle(angle) = constraint else {
11404            panic!("Expected angle constraint");
11405        };
11406        assert_eq!(angle.lines, vec![line2_id, line1_id]);
11407        assert_eq!(angle.sector, Some(3));
11408        assert_eq!(angle.inverse, Some(false));
11409        assert_eq!(angle.label_position, Some(label_position));
11410
11411        mock_ctx.close().await;
11412    }
11413
11414    #[tokio::test(flavor = "multi_thread")]
11415    async fn test_edit_angle_constraint_with_call_on_right() {
11416        let initial_source = "\
11417sketch(on = XY) {
11418  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11419  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11420  60deg == angle([line1, line2])
11421}
11422";
11423
11424        let program = Program::parse(initial_source).unwrap().0.unwrap();
11425        let mut frontend = FrontendState::new();
11426        let mock_ctx = ExecutorContext::new_mock(None).await;
11427        let version = Version(0);
11428
11429        frontend.program = program.clone();
11430        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11431        frontend.update_state_after_exec(outcome, true);
11432        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11433        let sketch_id = sketch_object.id;
11434        let sketch = expect_sketch(sketch_object);
11435        let constraint_id = sketch.constraints[0];
11436        let line1_id = *sketch.segments.get(2).unwrap();
11437        let line2_id = *sketch.segments.get(5).unwrap();
11438
11439        let (src_delta, _) = frontend
11440            .edit_angle_constraint_with_options(
11441                &mock_ctx,
11442                version,
11443                sketch_id,
11444                constraint_id,
11445                Angle {
11446                    lines: vec![line2_id, line1_id],
11447                    angle: Number {
11448                        value: 60.0,
11449                        units: NumericSuffix::Deg,
11450                    },
11451                    sector: Some(3),
11452                    inverse: Some(false),
11453                    label_position: None,
11454                    source: Default::default(),
11455                },
11456                EditConstraintOptions {
11457                    commit_solved_initial_guesses: false,
11458                },
11459            )
11460            .await
11461            .unwrap();
11462        assert_eq!(
11463            src_delta.text.as_str(),
11464            "\
11465sketch(on = XY) {
11466  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
11467  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
11468  60deg == angleDimension(lines = [line2, line1], sector = 3)
11469}
11470"
11471        );
11472
11473        mock_ctx.close().await;
11474    }
11475
11476    #[tokio::test(flavor = "multi_thread")]
11477    async fn test_edit_segments_can_commit_constraint_label_position_in_same_execution() {
11478        let initial_source = "\
11479@settings(kclVersion = 2.0)
11480
11481sketch001 = sketch(on = XZ) {
11482  line1 = line(start = [var 0mm, var 12.55mm], end = [var -6.03mm, var 8.51mm])
11483  line3 = line(start = [var -7.41mm, var 2.92mm], end = [var -1.47mm, var 4.32mm])
11484  distance([line1.start, line3.end], labelPosition = [5.56mm, 8.65mm]) == 8.36mm
11485  vertical([line1.start, ORIGIN])
11486}
11487";
11488
11489        let program = Program::parse(initial_source).unwrap().0.unwrap();
11490        let mut frontend = FrontendState::new();
11491        let mock_ctx = ExecutorContext::new_mock(None).await;
11492        let version = Version(0);
11493
11494        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
11495        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11496        let sketch_id = sketch_object.id;
11497        let sketch = expect_sketch(sketch_object);
11498        let constraint_id = sketch
11499            .constraints
11500            .iter()
11501            .copied()
11502            .find(|constraint_id| {
11503                matches!(
11504                    frontend.scene_graph.objects[constraint_id.0].kind,
11505                    ObjectKind::Constraint {
11506                        constraint: Constraint::Distance(_)
11507                    }
11508                )
11509            })
11510            .unwrap();
11511        let line1_id = sketch
11512            .segments
11513            .iter()
11514            .copied()
11515            .find(|segment_id| {
11516                matches!(
11517                    frontend.scene_graph.objects[segment_id.0].kind,
11518                    ObjectKind::Segment {
11519                        segment: Segment::Line(_)
11520                    }
11521                )
11522            })
11523            .unwrap();
11524        let label_position = Point2d {
11525            x: Number {
11526                value: 7.0,
11527                units: NumericSuffix::Mm,
11528            },
11529            y: Number {
11530                value: 9.0,
11531                units: NumericSuffix::Mm,
11532            },
11533        };
11534
11535        let (source_delta, scene_delta) = frontend
11536            .edit_segments_with_options(
11537                &mock_ctx,
11538                version,
11539                sketch_id,
11540                vec![ExistingSegmentCtor {
11541                    id: line1_id,
11542                    ctor: SegmentCtor::Line(LineCtor {
11543                        start: point_expr_mm(2.0, 15.55),
11544                        end: point_expr_mm(-4.03, 11.51),
11545                        construction: None,
11546                    }),
11547                }],
11548                EditSegmentsOptions {
11549                    anchor_segment_ids: Some(vec![]),
11550                    drag_anchors: vec![SegmentDragAnchor {
11551                        segment_id: line1_id,
11552                        target: label_position.clone(),
11553                    }],
11554                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11555                        constraint_id,
11556                        label_position: label_position.clone(),
11557                    }],
11558                    commit_solved_initial_guesses: true,
11559                },
11560            )
11561            .await
11562            .unwrap();
11563
11564        assert!(source_delta.text.contains("labelPosition = [7mm, 9mm]"));
11565        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11566        let ObjectKind::Constraint {
11567            constraint: Constraint::Distance(distance),
11568        } = &constraint_object.kind
11569        else {
11570            panic!("Expected distance constraint object");
11571        };
11572        assert_eq!(distance.label_position, Some(label_position));
11573
11574        let snapped_label_position = Point2d {
11575            x: Number {
11576                value: 8.0,
11577                units: NumericSuffix::Mm,
11578            },
11579            y: Number {
11580                value: 10.0,
11581                units: NumericSuffix::Mm,
11582            },
11583        };
11584        let (source_delta, scene_delta) = frontend
11585            .edit_segments_with_options(
11586                &mock_ctx,
11587                version,
11588                sketch_id,
11589                vec![],
11590                EditSegmentsOptions {
11591                    anchor_segment_ids: Some(vec![line1_id]),
11592                    drag_anchors: vec![],
11593                    constraint_label_edits: vec![ConstraintLabelPositionEdit {
11594                        constraint_id,
11595                        label_position: snapped_label_position.clone(),
11596                    }],
11597                    commit_solved_initial_guesses: true,
11598                },
11599            )
11600            .await
11601            .unwrap();
11602
11603        assert!(source_delta.text.contains("labelPosition = [8mm, 10mm]"));
11604        let constraint_object = &scene_delta.new_graph.objects[constraint_id.0];
11605        let ObjectKind::Constraint {
11606            constraint: Constraint::Distance(distance),
11607        } = &constraint_object.kind
11608        else {
11609            panic!("Expected distance constraint object");
11610        };
11611        assert_eq!(distance.label_position, Some(snapped_label_position));
11612
11613        mock_ctx.close().await;
11614    }
11615
11616    #[tokio::test(flavor = "multi_thread")]
11617    async fn test_edit_distance_constraint_label_position_preserves_anchor_segment_solution() {
11618        let initial_source = "\
11619sketch(on = XY) {
11620  point1 = point(at = [var 0mm, var 0mm])
11621  point2 = point(at = [var 10mm, var 0mm])
11622  distance([point1, point2]) == 5mm
11623}
11624";
11625
11626        let program = Program::parse(initial_source).unwrap().0.unwrap();
11627        let mut frontend = FrontendState::new();
11628        let mock_ctx = ExecutorContext::new_mock(None).await;
11629        let version = Version(0);
11630
11631        frontend.program = program.clone();
11632        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11633        frontend.update_state_after_exec(outcome, true);
11634        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11635        let sketch_id = sketch_object.id;
11636        let sketch = expect_sketch(sketch_object);
11637        let point0_id = sketch.segments[0];
11638        let point1_id = sketch.segments[1];
11639        let constraint_id = sketch.constraints[0];
11640
11641        let edited_segments = vec![ExistingSegmentCtor {
11642            id: point0_id,
11643            ctor: SegmentCtor::Point(PointCtor {
11644                position: Point2d {
11645                    x: Expr::Var(Number {
11646                        value: 2.0,
11647                        units: NumericSuffix::Mm,
11648                    }),
11649                    y: Expr::Var(Number {
11650                        value: 1.0,
11651                        units: NumericSuffix::Mm,
11652                    }),
11653                },
11654            }),
11655        }];
11656        let (_, scene_delta) = frontend
11657            .edit_segments(&mock_ctx, version, sketch_id, edited_segments)
11658            .await
11659            .unwrap();
11660        let point0_after_segment_edit = point_position(&scene_delta.new_graph, point0_id);
11661        let point1_after_segment_edit = point_position(&scene_delta.new_graph, point1_id);
11662
11663        let label_position = Point2d {
11664            x: Number {
11665                value: 3.0,
11666                units: NumericSuffix::Mm,
11667            },
11668            y: Number {
11669                value: 4.0,
11670                units: NumericSuffix::Mm,
11671            },
11672        };
11673        let (_, scene_delta) = frontend
11674            .edit_distance_constraint_label_position(
11675                &mock_ctx,
11676                version,
11677                sketch_id,
11678                constraint_id,
11679                label_position,
11680                vec![point0_id],
11681            )
11682            .await
11683            .unwrap();
11684
11685        assert_point_position_close(
11686            point_position(&scene_delta.new_graph, point0_id),
11687            point0_after_segment_edit,
11688        );
11689        assert_point_position_close(
11690            point_position(&scene_delta.new_graph, point1_id),
11691            point1_after_segment_edit,
11692        );
11693
11694        mock_ctx.close().await;
11695    }
11696
11697    #[tokio::test(flavor = "multi_thread")]
11698    async fn test_distance_point_line() {
11699        let initial_source = "\
11700sketch(on = XY) {
11701  point(at = [var 0, var 5])
11702  line(start = [var 0, var 0], end = [var 10, var 0])
11703}
11704";
11705
11706        let program = Program::parse(initial_source).unwrap().0.unwrap();
11707
11708        let mut frontend = FrontendState::new();
11709
11710        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11711        let mock_ctx = ExecutorContext::new_mock(None).await;
11712        let version = Version(0);
11713
11714        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
11715        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
11716        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11717        let sketch_id = sketch_object.id;
11718        let sketch = expect_sketch(sketch_object);
11719        let point_id = *sketch.segments.first().unwrap();
11720        let line_id = *sketch
11721            .segments
11722            .iter()
11723            .find(|segment_id| {
11724                matches!(
11725                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11726                    Some(ObjectKind::Segment {
11727                        segment: Segment::Line(_)
11728                    })
11729                )
11730            })
11731            .unwrap();
11732
11733        let label_position = Point2d {
11734            x: Number {
11735                value: 10.0,
11736                units: NumericSuffix::Mm,
11737            },
11738            y: Number {
11739                value: 11.0,
11740                units: NumericSuffix::Mm,
11741            },
11742        };
11743        let constraint = Constraint::Distance(Distance {
11744            segments: vec![point_id.into(), line_id.into()],
11745            distance: Number {
11746                value: 5.0,
11747                units: NumericSuffix::Mm,
11748            },
11749            label_position: Some(label_position.clone()),
11750            source: Default::default(),
11751        });
11752        let (src_delta, scene_delta) = frontend
11753            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11754            .await
11755            .unwrap();
11756        insta::assert_snapshot!("test_distance_point_line", src_delta.text.as_str());
11757        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
11758        let sketch = expect_sketch(sketch_object);
11759        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
11760        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
11761            panic!("Expected constraint object");
11762        };
11763        let Constraint::Distance(distance) = constraint else {
11764            panic!("Expected distance constraint");
11765        };
11766        assert_eq!(distance.label_position, Some(label_position));
11767
11768        ctx.close().await;
11769        mock_ctx.close().await;
11770    }
11771
11772    #[tokio::test(flavor = "multi_thread")]
11773    async fn test_distance_point_arc() {
11774        let initial_source = "\
11775sketch(on = XY) {
11776  point(at = [var 0, var 8])
11777  arc(start = [var 5, var 0], end = [var 0, var 5], center = [var 0, var 0])
11778}
11779";
11780
11781        let program = Program::parse(initial_source).unwrap().0.unwrap();
11782
11783        let mut frontend = FrontendState::new();
11784
11785        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11786        let mock_ctx = ExecutorContext::new_mock(None).await;
11787        let version = Version(0);
11788
11789        frontend.hack_set_program(&ctx, program).await.unwrap();
11790        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11791        let sketch_id = sketch_object.id;
11792        let sketch = expect_sketch(sketch_object);
11793        let point_id = *sketch.segments.first().unwrap();
11794        let arc_id = *sketch
11795            .segments
11796            .iter()
11797            .find(|segment_id| {
11798                matches!(
11799                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11800                    Some(ObjectKind::Segment {
11801                        segment: Segment::Arc(_)
11802                    })
11803                )
11804            })
11805            .unwrap();
11806
11807        let constraint = Constraint::Distance(Distance {
11808            segments: vec![point_id.into(), arc_id.into()],
11809            distance: Number {
11810                value: 3.0,
11811                units: NumericSuffix::Mm,
11812            },
11813            label_position: None,
11814            source: Default::default(),
11815        });
11816        let (src_delta, _scene_delta) = frontend
11817            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11818            .await
11819            .unwrap();
11820        insta::assert_snapshot!("test_distance_point_arc", src_delta.text.as_str());
11821
11822        ctx.close().await;
11823        mock_ctx.close().await;
11824    }
11825
11826    #[tokio::test(flavor = "multi_thread")]
11827    async fn test_distance_arc_origin() {
11828        let initial_source = "\
11829sketch001 = sketch(on = XY) {
11830  arc(start = [var -4.13mm, var -0.59mm], end = [var -3.47mm, var 3.38mm], center = [var -4.55mm, var 1.52mm])
11831}
11832";
11833
11834        let program = Program::parse(initial_source).unwrap().0.unwrap();
11835
11836        let mut frontend = FrontendState::new();
11837
11838        let mock_ctx = ExecutorContext::new_mock(None).await;
11839        let version = Version(0);
11840
11841        frontend.program = program.clone();
11842        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11843        frontend.update_state_after_exec(outcome, true);
11844        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11845        let sketch_id = sketch_object.id;
11846        let sketch = expect_sketch(sketch_object);
11847        let arc_id = *sketch
11848            .segments
11849            .iter()
11850            .find(|segment_id| {
11851                matches!(
11852                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11853                    Some(ObjectKind::Segment {
11854                        segment: Segment::Arc(_)
11855                    })
11856                )
11857            })
11858            .unwrap();
11859
11860        let constraint = Constraint::Distance(Distance {
11861            segments: vec![arc_id.into(), ConstraintSegment::ORIGIN],
11862            distance: Number {
11863                value: 3.0,
11864                units: NumericSuffix::Mm,
11865            },
11866            label_position: None,
11867            source: Default::default(),
11868        });
11869        let (src_delta, _scene_delta) = frontend
11870            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11871            .await
11872            .unwrap();
11873        insta::assert_snapshot!("test_distance_arc_origin", src_delta.text.as_str());
11874
11875        mock_ctx.close().await;
11876    }
11877
11878    #[tokio::test(flavor = "multi_thread")]
11879    async fn test_distance_line_origin() {
11880        let initial_source = "\
11881sketch(on = XY) {
11882  line(start = [var 5, var 0], end = [var 5, var 10])
11883}
11884";
11885
11886        let program = Program::parse(initial_source).unwrap().0.unwrap();
11887
11888        let mut frontend = FrontendState::new();
11889
11890        let mock_ctx = ExecutorContext::new_mock(None).await;
11891        let version = Version(0);
11892
11893        frontend.program = program.clone();
11894        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
11895        frontend.update_state_after_exec(outcome, true);
11896        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11897        let sketch_id = sketch_object.id;
11898        let sketch = expect_sketch(sketch_object);
11899        let line_id = *sketch
11900            .segments
11901            .iter()
11902            .find(|segment_id| {
11903                matches!(
11904                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11905                    Some(ObjectKind::Segment {
11906                        segment: Segment::Line(_)
11907                    })
11908                )
11909            })
11910            .unwrap();
11911
11912        let constraint = Constraint::Distance(Distance {
11913            segments: vec![ConstraintSegment::ORIGIN, line_id.into()],
11914            distance: Number {
11915                value: 5.0,
11916                units: NumericSuffix::Mm,
11917            },
11918            label_position: None,
11919            source: Default::default(),
11920        });
11921        let (src_delta, _scene_delta) = frontend
11922            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11923            .await
11924            .unwrap();
11925        insta::assert_snapshot!("test_distance_line_origin", src_delta.text.as_str());
11926
11927        mock_ctx.close().await;
11928    }
11929
11930    #[tokio::test(flavor = "multi_thread")]
11931    async fn test_distance_line_circle() {
11932        let initial_source = "\
11933sketch(on = XY) {
11934  line(start = [var -10, var 8], end = [var 10, var 8])
11935  circle(start = [var 5, var 0], center = [var 0, var 0])
11936}
11937";
11938
11939        let program = Program::parse(initial_source).unwrap().0.unwrap();
11940
11941        let mut frontend = FrontendState::new();
11942
11943        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
11944        let mock_ctx = ExecutorContext::new_mock(None).await;
11945        let version = Version(0);
11946
11947        frontend.hack_set_program(&ctx, program).await.unwrap();
11948        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
11949        let sketch_id = sketch_object.id;
11950        let sketch = expect_sketch(sketch_object);
11951        let line_id = *sketch
11952            .segments
11953            .iter()
11954            .find(|segment_id| {
11955                matches!(
11956                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11957                    Some(ObjectKind::Segment {
11958                        segment: Segment::Line(_)
11959                    })
11960                )
11961            })
11962            .unwrap();
11963        let circle_id = *sketch
11964            .segments
11965            .iter()
11966            .find(|segment_id| {
11967                matches!(
11968                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
11969                    Some(ObjectKind::Segment {
11970                        segment: Segment::Circle(_)
11971                    })
11972                )
11973            })
11974            .unwrap();
11975
11976        let constraint = Constraint::Distance(Distance {
11977            segments: vec![line_id.into(), circle_id.into()],
11978            distance: Number {
11979                value: 3.0,
11980                units: NumericSuffix::Mm,
11981            },
11982            label_position: None,
11983            source: Default::default(),
11984        });
11985        let (src_delta, _scene_delta) = frontend
11986            .add_constraint(&mock_ctx, version, sketch_id, constraint)
11987            .await
11988            .unwrap();
11989        insta::assert_snapshot!("test_distance_line_circle", src_delta.text.as_str());
11990
11991        ctx.close().await;
11992        mock_ctx.close().await;
11993    }
11994
11995    #[tokio::test(flavor = "multi_thread")]
11996    async fn test_distance_circle_arc() {
11997        let initial_source = "\
11998sketch(on = XY) {
11999  circle(start = [var 5, var 0], center = [var 0, var 0])
12000  arc(start = [var 15, var 0], end = [var 10, var 5], center = [var 10, var 0])
12001}
12002";
12003
12004        let program = Program::parse(initial_source).unwrap().0.unwrap();
12005
12006        let mut frontend = FrontendState::new();
12007
12008        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12009        let mock_ctx = ExecutorContext::new_mock(None).await;
12010        let version = Version(0);
12011
12012        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
12013        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
12014        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12015        let sketch_id = sketch_object.id;
12016        let sketch = expect_sketch(sketch_object);
12017        let circle_id = *sketch
12018            .segments
12019            .iter()
12020            .find(|segment_id| {
12021                matches!(
12022                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12023                    Some(ObjectKind::Segment {
12024                        segment: Segment::Circle(_)
12025                    })
12026                )
12027            })
12028            .unwrap();
12029        let arc_id = *sketch
12030            .segments
12031            .iter()
12032            .find(|segment_id| {
12033                matches!(
12034                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12035                    Some(ObjectKind::Segment {
12036                        segment: Segment::Arc(_)
12037                    })
12038                )
12039            })
12040            .unwrap();
12041
12042        let constraint = Constraint::Distance(Distance {
12043            segments: vec![circle_id.into(), arc_id.into()],
12044            distance: Number {
12045                value: 3.0,
12046                units: NumericSuffix::Mm,
12047            },
12048            label_position: None,
12049            source: Default::default(),
12050        });
12051        let (src_delta, _scene_delta) = frontend
12052            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12053            .await
12054            .unwrap();
12055        insta::assert_snapshot!("test_distance_circle_arc", src_delta.text.as_str());
12056
12057        ctx.close().await;
12058        mock_ctx.close().await;
12059    }
12060
12061    #[tokio::test(flavor = "multi_thread")]
12062    async fn test_distance_parallel_lines() {
12063        let initial_source = "\
12064sketch(on = XY) {
12065  line(start = [var 0, var 0], end = [var 10, var 0])
12066  line(start = [var 0, var 5], end = [var 10, var 5])
12067}
12068";
12069
12070        let program = Program::parse(initial_source).unwrap().0.unwrap();
12071
12072        let mut frontend = FrontendState::new();
12073
12074        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12075        let mock_ctx = ExecutorContext::new_mock(None).await;
12076        let version = Version(0);
12077
12078        frontend.hack_set_program(&ctx, program).await.unwrap();
12079        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12080        let sketch_id = sketch_object.id;
12081        let sketch = expect_sketch(sketch_object);
12082        let line_ids = sketch
12083            .segments
12084            .iter()
12085            .copied()
12086            .filter(|segment_id| {
12087                matches!(
12088                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12089                    Some(ObjectKind::Segment {
12090                        segment: Segment::Line(_)
12091                    })
12092                )
12093            })
12094            .collect::<Vec<_>>();
12095
12096        let constraint = Constraint::Distance(Distance {
12097            segments: vec![line_ids[0].into(), line_ids[1].into()],
12098            distance: Number {
12099                value: 5.0,
12100                units: NumericSuffix::Mm,
12101            },
12102            label_position: None,
12103            source: Default::default(),
12104        });
12105        let (src_delta, _scene_delta) = frontend
12106            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12107            .await
12108            .unwrap();
12109        insta::assert_snapshot!("test_distance_parallel_lines", src_delta.text.as_str());
12110
12111        ctx.close().await;
12112        mock_ctx.close().await;
12113    }
12114
12115    #[tokio::test(flavor = "multi_thread")]
12116    async fn test_distance_non_parallel_lines_lowers_to_distance() {
12117        // NOTE: Current LinesAtAngle constraint collapses if lines are initialized perpendicular to
12118        // one another because the gradient of the residual has no tangential component in this
12119        // configuration. The only path to reducing the residual is shrinking the lengths of the
12120        // lines which are causing the lines to collapse, producing a degenerate output.
12121        let initial_source = "\
12122sketch(on = XY) {
12123  line(start = [var 0, var 0], end = [var 10, var 0])
12124  line(start = [var 0, var 0], end = [var 10, var 10])
12125}
12126";
12127
12128        let program = Program::parse(initial_source).unwrap().0.unwrap();
12129
12130        let mut frontend = FrontendState::new();
12131
12132        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12133        let mock_ctx = ExecutorContext::new_mock(None).await;
12134        let version = Version(0);
12135
12136        frontend.hack_set_program(&ctx, program).await.unwrap();
12137        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12138        let sketch_id = sketch_object.id;
12139        let sketch = expect_sketch(sketch_object);
12140        let line_ids = sketch
12141            .segments
12142            .iter()
12143            .copied()
12144            .filter(|segment_id| {
12145                matches!(
12146                    frontend.scene_graph.objects.get(segment_id.0).map(|obj| &obj.kind),
12147                    Some(ObjectKind::Segment {
12148                        segment: Segment::Line(_)
12149                    })
12150                )
12151            })
12152            .collect::<Vec<_>>();
12153
12154        let constraint = Constraint::Distance(Distance {
12155            segments: vec![line_ids[0].into(), line_ids[1].into()],
12156            distance: Number {
12157                value: 5.0,
12158                units: NumericSuffix::Mm,
12159            },
12160            label_position: None,
12161            source: Default::default(),
12162        });
12163        let (src_delta, _scene_delta) = frontend
12164            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12165            .await
12166            .unwrap();
12167        insta::assert_snapshot!(
12168            "test_distance_non_parallel_lines_lowers_to_distance",
12169            src_delta.text.as_str()
12170        );
12171
12172        ctx.close().await;
12173        mock_ctx.close().await;
12174    }
12175
12176    #[tokio::test(flavor = "multi_thread")]
12177    async fn test_horizontal_distance_two_points() {
12178        let initial_source = "\
12179sketch(on = XY) {
12180  point(at = [var 1, var 2])
12181  point(at = [var 3, var 4])
12182}
12183";
12184
12185        let program = Program::parse(initial_source).unwrap().0.unwrap();
12186
12187        let mut frontend = FrontendState::new();
12188
12189        let mock_ctx = ExecutorContext::new_mock(None).await;
12190        let version = Version(0);
12191
12192        frontend.program = program.clone();
12193        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12194        frontend.update_state_after_exec(outcome, true);
12195        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12196        let sketch_id = sketch_object.id;
12197        let sketch = expect_sketch(sketch_object);
12198        let point0_id = *sketch.segments.first().unwrap();
12199        let point1_id = *sketch.segments.get(1).unwrap();
12200        let label_position = Point2d {
12201            x: Number {
12202                value: 10.0,
12203                units: NumericSuffix::Mm,
12204            },
12205            y: Number {
12206                value: 11.0,
12207                units: NumericSuffix::Mm,
12208            },
12209        };
12210
12211        let constraint = Constraint::HorizontalDistance(Distance {
12212            segments: vec![point0_id.into(), point1_id.into()],
12213            distance: Number {
12214                value: 2.0,
12215                units: NumericSuffix::Mm,
12216            },
12217            label_position: Some(label_position.clone()),
12218            source: Default::default(),
12219        });
12220        let (src_delta, scene_delta) = frontend
12221            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12222            .await
12223            .unwrap();
12224        insta::assert_snapshot!("test_horizontal_distance_two_points", src_delta.text.as_str());
12225        assert_eq!(
12226            scene_delta.new_graph.objects.len(),
12227            5,
12228            "{:#?}",
12229            scene_delta.new_graph.objects
12230        );
12231        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12232        let sketch = expect_sketch(sketch_object);
12233        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12234        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12235            panic!("Expected constraint object");
12236        };
12237        let Constraint::HorizontalDistance(distance) = constraint else {
12238            panic!("Expected horizontal distance constraint");
12239        };
12240        assert_eq!(distance.label_position, Some(label_position));
12241
12242        mock_ctx.close().await;
12243    }
12244
12245    #[tokio::test(flavor = "multi_thread")]
12246    async fn test_radius_single_arc_segment() {
12247        let initial_source = "\
12248sketch(on = XY) {
12249  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12250}
12251";
12252
12253        let program = Program::parse(initial_source).unwrap().0.unwrap();
12254
12255        let mut frontend = FrontendState::new();
12256
12257        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12258        let mock_ctx = ExecutorContext::new_mock(None).await;
12259        let version = Version(0);
12260
12261        frontend.hack_set_program(&ctx, program).await.unwrap();
12262        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12263        let sketch_id = sketch_object.id;
12264        let sketch = expect_sketch(sketch_object);
12265        // Find the arc segment (not the points)
12266        let arc_id = sketch
12267            .segments
12268            .iter()
12269            .find(|&seg_id| {
12270                let obj = frontend.scene_graph.objects.get(seg_id.0);
12271                matches!(
12272                    obj.map(|o| &o.kind),
12273                    Some(ObjectKind::Segment {
12274                        segment: Segment::Arc(_)
12275                    })
12276                )
12277            })
12278            .unwrap();
12279
12280        let constraint = Constraint::Radius(Radius {
12281            arc: *arc_id,
12282            radius: Number {
12283                value: 5.0,
12284                units: NumericSuffix::Mm,
12285            },
12286            label_position: None,
12287            source: Default::default(),
12288        });
12289        let (src_delta, scene_delta) = frontend
12290            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12291            .await
12292            .unwrap();
12293        insta::assert_snapshot!("test_radius_single_arc_segment", src_delta.text.as_str());
12294        assert_eq!(
12295            scene_delta.new_graph.objects.len(),
12296            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12297            "{:#?}",
12298            scene_delta.new_graph.objects
12299        );
12300
12301        ctx.close().await;
12302        mock_ctx.close().await;
12303    }
12304
12305    #[tokio::test(flavor = "multi_thread")]
12306    async fn test_radius_single_arc_segment_with_label_position() {
12307        let initial_source = "\
12308sketch(on = XY) {
12309  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12310}
12311";
12312
12313        let program = Program::parse(initial_source).unwrap().0.unwrap();
12314        let mut frontend = FrontendState::new();
12315        let mock_ctx = ExecutorContext::new_mock(None).await;
12316        let version = Version(0);
12317
12318        frontend.program = program.clone();
12319        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12320        frontend.update_state_after_exec(outcome, true);
12321        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12322        let sketch_id = sketch_object.id;
12323        let sketch = expect_sketch(sketch_object);
12324        let arc_id = sketch
12325            .segments
12326            .iter()
12327            .find(|&seg_id| {
12328                let obj = frontend.scene_graph.objects.get(seg_id.0);
12329                matches!(
12330                    obj.map(|o| &o.kind),
12331                    Some(ObjectKind::Segment {
12332                        segment: Segment::Arc(_)
12333                    })
12334                )
12335            })
12336            .unwrap();
12337
12338        let label_position = Point2d {
12339            x: Number {
12340                value: 10.0,
12341                units: NumericSuffix::Mm,
12342            },
12343            y: Number {
12344                value: 11.0,
12345                units: NumericSuffix::Mm,
12346            },
12347        };
12348        let constraint = Constraint::Radius(Radius {
12349            arc: *arc_id,
12350            radius: Number {
12351                value: 5.0,
12352                units: NumericSuffix::Mm,
12353            },
12354            label_position: Some(label_position.clone()),
12355            source: Default::default(),
12356        });
12357        let (src_delta, scene_delta) = frontend
12358            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12359            .await
12360            .unwrap();
12361        insta::assert_snapshot!(
12362            "test_radius_single_arc_segment_with_label_position",
12363            src_delta.text.as_str()
12364        );
12365
12366        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12367        let sketch = expect_sketch(sketch_object);
12368        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12369        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12370            panic!("Expected constraint object");
12371        };
12372        let Constraint::Radius(radius) = constraint else {
12373            panic!("Expected radius constraint");
12374        };
12375        assert_eq!(radius.label_position, Some(label_position));
12376
12377        mock_ctx.close().await;
12378    }
12379
12380    #[tokio::test(flavor = "multi_thread")]
12381    async fn test_edit_radius_constraint_label_position() {
12382        let initial_source = "\
12383sketch(on = XY) {
12384  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12385  radius(arc1) == 5mm
12386}
12387";
12388
12389        let program = Program::parse(initial_source).unwrap().0.unwrap();
12390        let mut frontend = FrontendState::new();
12391        let mock_ctx = ExecutorContext::new_mock(None).await;
12392        let version = Version(0);
12393
12394        frontend.program = program.clone();
12395        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12396        frontend.update_state_after_exec(outcome, true);
12397        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12398        let sketch_id = sketch_object.id;
12399        let sketch = expect_sketch(sketch_object);
12400        let constraint_id = sketch.constraints[0];
12401        let label_position = Point2d {
12402            x: Number {
12403                value: 10.0,
12404                units: NumericSuffix::Mm,
12405            },
12406            y: Number {
12407                value: 11.0,
12408                units: NumericSuffix::Mm,
12409            },
12410        };
12411
12412        let (src_delta, scene_delta) = frontend
12413            .edit_distance_constraint_label_position(
12414                &mock_ctx,
12415                version,
12416                sketch_id,
12417                constraint_id,
12418                label_position.clone(),
12419                vec![],
12420            )
12421            .await
12422            .unwrap();
12423        insta::assert_snapshot!("test_edit_radius_constraint_label_position", src_delta.text.as_str());
12424
12425        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12426        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12427            panic!("Expected constraint object");
12428        };
12429        let Constraint::Radius(radius) = constraint else {
12430            panic!("Expected radius constraint");
12431        };
12432        assert_eq!(radius.label_position, Some(label_position));
12433
12434        mock_ctx.close().await;
12435    }
12436
12437    #[tokio::test(flavor = "multi_thread")]
12438    async fn test_vertical_distance_two_points() {
12439        let initial_source = "\
12440sketch(on = XY) {
12441  point(at = [var 1, var 2])
12442  point(at = [var 3, var 4])
12443}
12444";
12445
12446        let program = Program::parse(initial_source).unwrap().0.unwrap();
12447
12448        let mut frontend = FrontendState::new();
12449
12450        let mock_ctx = ExecutorContext::new_mock(None).await;
12451        let version = Version(0);
12452
12453        frontend.program = program.clone();
12454        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12455        frontend.update_state_after_exec(outcome, true);
12456        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12457        let sketch_id = sketch_object.id;
12458        let sketch = expect_sketch(sketch_object);
12459        let point0_id = *sketch.segments.first().unwrap();
12460        let point1_id = *sketch.segments.get(1).unwrap();
12461        let label_position = Point2d {
12462            x: Number {
12463                value: 10.0,
12464                units: NumericSuffix::Mm,
12465            },
12466            y: Number {
12467                value: 11.0,
12468                units: NumericSuffix::Mm,
12469            },
12470        };
12471
12472        let constraint = Constraint::VerticalDistance(Distance {
12473            segments: vec![point0_id.into(), point1_id.into()],
12474            distance: Number {
12475                value: 2.0,
12476                units: NumericSuffix::Mm,
12477            },
12478            label_position: Some(label_position.clone()),
12479            source: Default::default(),
12480        });
12481        let (src_delta, scene_delta) = frontend
12482            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12483            .await
12484            .unwrap();
12485        insta::assert_snapshot!("test_vertical_distance_two_points", src_delta.text.as_str());
12486        assert_eq!(
12487            scene_delta.new_graph.objects.len(),
12488            5,
12489            "{:#?}",
12490            scene_delta.new_graph.objects
12491        );
12492        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12493        let sketch = expect_sketch(sketch_object);
12494        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12495        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12496            panic!("Expected constraint object");
12497        };
12498        let Constraint::VerticalDistance(distance) = constraint else {
12499            panic!("Expected vertical distance constraint");
12500        };
12501        assert_eq!(distance.label_position, Some(label_position));
12502
12503        mock_ctx.close().await;
12504    }
12505
12506    #[tokio::test(flavor = "multi_thread")]
12507    async fn test_add_fixed_standalone_point() {
12508        let initial_source = "\
12509sketch(on = XY) {
12510  point(at = [var 1, var 2])
12511}
12512";
12513
12514        let program = Program::parse(initial_source).unwrap().0.unwrap();
12515
12516        let mut frontend = FrontendState::new();
12517
12518        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12519        let mock_ctx = ExecutorContext::new_mock(None).await;
12520        let version = Version(0);
12521
12522        frontend.hack_set_program(&ctx, program).await.unwrap();
12523        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12524        let sketch_id = sketch_object.id;
12525        let sketch = expect_sketch(sketch_object);
12526        let point_id = *sketch.segments.first().unwrap();
12527
12528        let (src_delta, scene_delta) = frontend
12529            .add_constraint(
12530                &mock_ctx,
12531                version,
12532                sketch_id,
12533                Constraint::Fixed(Fixed {
12534                    points: vec![FixedPoint {
12535                        point: point_id,
12536                        position: Point2d {
12537                            x: Number {
12538                                value: 2.0,
12539                                units: NumericSuffix::Mm,
12540                            },
12541                            y: Number {
12542                                value: 3.0,
12543                                units: NumericSuffix::Mm,
12544                            },
12545                        },
12546                    }],
12547                }),
12548            )
12549            .await
12550            .unwrap();
12551        insta::assert_snapshot!("test_add_fixed_standalone_point", src_delta.text.as_str());
12552        assert_eq!(
12553            scene_delta.new_graph.objects.len(),
12554            4,
12555            "{:#?}",
12556            scene_delta.new_graph.objects
12557        );
12558
12559        ctx.close().await;
12560        mock_ctx.close().await;
12561    }
12562
12563    #[tokio::test(flavor = "multi_thread")]
12564    async fn test_add_fixed_multiple_points() {
12565        let initial_source = "\
12566sketch(on = XY) {
12567  point(at = [var 1, var 2])
12568  point(at = [var 3, var 4])
12569}
12570";
12571
12572        let program = Program::parse(initial_source).unwrap().0.unwrap();
12573
12574        let mut frontend = FrontendState::new();
12575
12576        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12577        let mock_ctx = ExecutorContext::new_mock(None).await;
12578        let version = Version(0);
12579
12580        frontend.hack_set_program(&ctx, program).await.unwrap();
12581        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12582        let sketch_id = sketch_object.id;
12583        let sketch = expect_sketch(sketch_object);
12584        let point0_id = *sketch.segments.first().unwrap();
12585        let point1_id = *sketch.segments.get(1).unwrap();
12586
12587        let (src_delta, scene_delta) = frontend
12588            .add_constraint(
12589                &mock_ctx,
12590                version,
12591                sketch_id,
12592                Constraint::Fixed(Fixed {
12593                    points: vec![
12594                        FixedPoint {
12595                            point: point0_id,
12596                            position: Point2d {
12597                                x: Number {
12598                                    value: 2.0,
12599                                    units: NumericSuffix::Mm,
12600                                },
12601                                y: Number {
12602                                    value: 3.0,
12603                                    units: NumericSuffix::Mm,
12604                                },
12605                            },
12606                        },
12607                        FixedPoint {
12608                            point: point1_id,
12609                            position: Point2d {
12610                                x: Number {
12611                                    value: 4.0,
12612                                    units: NumericSuffix::Mm,
12613                                },
12614                                y: Number {
12615                                    value: 5.0,
12616                                    units: NumericSuffix::Mm,
12617                                },
12618                            },
12619                        },
12620                    ],
12621                }),
12622            )
12623            .await
12624            .unwrap();
12625        insta::assert_snapshot!("test_add_fixed_multiple_points", src_delta.text.as_str());
12626        assert_eq!(
12627            scene_delta.new_graph.objects.len(),
12628            6,
12629            "{:#?}",
12630            scene_delta.new_graph.objects
12631        );
12632
12633        ctx.close().await;
12634        mock_ctx.close().await;
12635    }
12636
12637    #[tokio::test(flavor = "multi_thread")]
12638    async fn test_add_fixed_owned_point() {
12639        let initial_source = "\
12640sketch(on = XY) {
12641  line(start = [var 1, var 2], end = [var 3, var 4])
12642}
12643";
12644
12645        let program = Program::parse(initial_source).unwrap().0.unwrap();
12646
12647        let mut frontend = FrontendState::new();
12648
12649        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12650        let mock_ctx = ExecutorContext::new_mock(None).await;
12651        let version = Version(0);
12652
12653        frontend.hack_set_program(&ctx, program).await.unwrap();
12654        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12655        let sketch_id = sketch_object.id;
12656        let sketch = expect_sketch(sketch_object);
12657        let line_start_id = *sketch.segments.first().unwrap();
12658
12659        let (src_delta, scene_delta) = frontend
12660            .add_constraint(
12661                &mock_ctx,
12662                version,
12663                sketch_id,
12664                Constraint::Fixed(Fixed {
12665                    points: vec![FixedPoint {
12666                        point: line_start_id,
12667                        position: Point2d {
12668                            x: Number {
12669                                value: 2.0,
12670                                units: NumericSuffix::Mm,
12671                            },
12672                            y: Number {
12673                                value: 3.0,
12674                                units: NumericSuffix::Mm,
12675                            },
12676                        },
12677                    }],
12678                }),
12679            )
12680            .await
12681            .unwrap();
12682        insta::assert_snapshot!("test_add_fixed_owned_point", src_delta.text.as_str());
12683        assert_eq!(
12684            scene_delta.new_graph.objects.len(),
12685            6,
12686            "{:#?}",
12687            scene_delta.new_graph.objects
12688        );
12689
12690        ctx.close().await;
12691        mock_ctx.close().await;
12692    }
12693
12694    #[tokio::test(flavor = "multi_thread")]
12695    async fn test_radius_error_cases() {
12696        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12697        let mock_ctx = ExecutorContext::new_mock(None).await;
12698        let version = Version(0);
12699
12700        // Test: Single point should error
12701        let initial_source_point = "\
12702sketch(on = XY) {
12703  point(at = [var 1, var 2])
12704}
12705";
12706        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12707        let mut frontend_point = FrontendState::new();
12708        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12709        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12710        let sketch_id_point = sketch_object_point.id;
12711        let sketch_point = expect_sketch(sketch_object_point);
12712        let point_id = *sketch_point.segments.first().unwrap();
12713
12714        let constraint_point = Constraint::Radius(Radius {
12715            arc: point_id,
12716            radius: Number {
12717                value: 5.0,
12718                units: NumericSuffix::Mm,
12719            },
12720            label_position: None,
12721            source: Default::default(),
12722        });
12723        let result_point = frontend_point
12724            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12725            .await;
12726        assert!(result_point.is_err(), "Single point should error for radius");
12727
12728        // Test: Single line segment should error (only arc segments supported)
12729        let initial_source_line = "\
12730sketch(on = XY) {
12731  line(start = [var 1, var 2], end = [var 3, var 4])
12732}
12733";
12734        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12735        let mut frontend_line = FrontendState::new();
12736        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12737        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12738        let sketch_id_line = sketch_object_line.id;
12739        let sketch_line = expect_sketch(sketch_object_line);
12740        let line_id = *sketch_line.segments.first().unwrap();
12741
12742        let constraint_line = Constraint::Radius(Radius {
12743            arc: line_id,
12744            radius: Number {
12745                value: 5.0,
12746                units: NumericSuffix::Mm,
12747            },
12748            label_position: None,
12749            source: Default::default(),
12750        });
12751        let result_line = frontend_line
12752            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
12753            .await;
12754        assert!(result_line.is_err(), "Single line segment should error for radius");
12755
12756        ctx.close().await;
12757        mock_ctx.close().await;
12758    }
12759
12760    #[tokio::test(flavor = "multi_thread")]
12761    async fn test_diameter_single_arc_segment() {
12762        let initial_source = "\
12763sketch(on = XY) {
12764  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12765}
12766";
12767
12768        let program = Program::parse(initial_source).unwrap().0.unwrap();
12769
12770        let mut frontend = FrontendState::new();
12771
12772        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12773        let mock_ctx = ExecutorContext::new_mock(None).await;
12774        let version = Version(0);
12775
12776        frontend.hack_set_program(&ctx, program).await.unwrap();
12777        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12778        let sketch_id = sketch_object.id;
12779        let sketch = expect_sketch(sketch_object);
12780        // Find the arc segment (not the points)
12781        let arc_id = sketch
12782            .segments
12783            .iter()
12784            .find(|&seg_id| {
12785                let obj = frontend.scene_graph.objects.get(seg_id.0);
12786                matches!(
12787                    obj.map(|o| &o.kind),
12788                    Some(ObjectKind::Segment {
12789                        segment: Segment::Arc(_)
12790                    })
12791                )
12792            })
12793            .unwrap();
12794
12795        let constraint = Constraint::Diameter(Diameter {
12796            arc: *arc_id,
12797            diameter: Number {
12798                value: 10.0,
12799                units: NumericSuffix::Mm,
12800            },
12801            label_position: None,
12802            source: Default::default(),
12803        });
12804        let (src_delta, scene_delta) = frontend
12805            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12806            .await
12807            .unwrap();
12808        insta::assert_snapshot!("test_diameter_single_arc_segment", src_delta.text.as_str());
12809        assert_eq!(
12810            scene_delta.new_graph.objects.len(),
12811            7, // Plane (0) + Sketch (1) + Start point (2) + End point (3) + Center point (4) + Arc (5) + Constraint (6)
12812            "{:#?}",
12813            scene_delta.new_graph.objects
12814        );
12815
12816        ctx.close().await;
12817        mock_ctx.close().await;
12818    }
12819
12820    #[tokio::test(flavor = "multi_thread")]
12821    async fn test_diameter_single_arc_segment_with_label_position() {
12822        let initial_source = "\
12823sketch(on = XY) {
12824  arc(start = [var 1, var 2], end = [var 3, var 4], center = [var 0, var 0])
12825}
12826";
12827
12828        let program = Program::parse(initial_source).unwrap().0.unwrap();
12829        let mut frontend = FrontendState::new();
12830        let mock_ctx = ExecutorContext::new_mock(None).await;
12831        let version = Version(0);
12832
12833        frontend.program = program.clone();
12834        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12835        frontend.update_state_after_exec(outcome, true);
12836        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12837        let sketch_id = sketch_object.id;
12838        let sketch = expect_sketch(sketch_object);
12839        let arc_id = sketch
12840            .segments
12841            .iter()
12842            .find(|&seg_id| {
12843                let obj = frontend.scene_graph.objects.get(seg_id.0);
12844                matches!(
12845                    obj.map(|o| &o.kind),
12846                    Some(ObjectKind::Segment {
12847                        segment: Segment::Arc(_)
12848                    })
12849                )
12850            })
12851            .unwrap();
12852
12853        let label_position = Point2d {
12854            x: Number {
12855                value: 10.0,
12856                units: NumericSuffix::Mm,
12857            },
12858            y: Number {
12859                value: 11.0,
12860                units: NumericSuffix::Mm,
12861            },
12862        };
12863        let constraint = Constraint::Diameter(Diameter {
12864            arc: *arc_id,
12865            diameter: Number {
12866                value: 10.0,
12867                units: NumericSuffix::Mm,
12868            },
12869            label_position: Some(label_position.clone()),
12870            source: Default::default(),
12871        });
12872        let (src_delta, scene_delta) = frontend
12873            .add_constraint(&mock_ctx, version, sketch_id, constraint)
12874            .await
12875            .unwrap();
12876        insta::assert_snapshot!(
12877            "test_diameter_single_arc_segment_with_label_position",
12878            src_delta.text.as_str()
12879        );
12880
12881        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
12882        let sketch = expect_sketch(sketch_object);
12883        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
12884        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12885            panic!("Expected constraint object");
12886        };
12887        let Constraint::Diameter(diameter) = constraint else {
12888            panic!("Expected diameter constraint");
12889        };
12890        assert_eq!(diameter.label_position, Some(label_position));
12891
12892        mock_ctx.close().await;
12893    }
12894
12895    #[tokio::test(flavor = "multi_thread")]
12896    async fn test_edit_diameter_constraint_label_position() {
12897        let initial_source = "\
12898sketch(on = XY) {
12899  arc1 = arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])
12900  diameter(arc1) == 10mm
12901}
12902";
12903
12904        let program = Program::parse(initial_source).unwrap().0.unwrap();
12905        let mut frontend = FrontendState::new();
12906        let mock_ctx = ExecutorContext::new_mock(None).await;
12907        let version = Version(0);
12908
12909        frontend.program = program.clone();
12910        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
12911        frontend.update_state_after_exec(outcome, true);
12912        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
12913        let sketch_id = sketch_object.id;
12914        let sketch = expect_sketch(sketch_object);
12915        let constraint_id = sketch.constraints[0];
12916        let label_position = Point2d {
12917            x: Number {
12918                value: 10.0,
12919                units: NumericSuffix::Mm,
12920            },
12921            y: Number {
12922                value: 11.0,
12923                units: NumericSuffix::Mm,
12924            },
12925        };
12926
12927        let (src_delta, scene_delta) = frontend
12928            .edit_distance_constraint_label_position(
12929                &mock_ctx,
12930                version,
12931                sketch_id,
12932                constraint_id,
12933                label_position.clone(),
12934                vec![],
12935            )
12936            .await
12937            .unwrap();
12938        insta::assert_snapshot!("test_edit_diameter_constraint_label_position", src_delta.text.as_str());
12939
12940        let constraint_object = scene_delta.new_graph.objects.get(constraint_id.0).unwrap();
12941        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
12942            panic!("Expected constraint object");
12943        };
12944        let Constraint::Diameter(diameter) = constraint else {
12945            panic!("Expected diameter constraint");
12946        };
12947        assert_eq!(diameter.label_position, Some(label_position));
12948
12949        mock_ctx.close().await;
12950    }
12951
12952    #[tokio::test(flavor = "multi_thread")]
12953    async fn test_diameter_error_cases() {
12954        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
12955        let mock_ctx = ExecutorContext::new_mock(None).await;
12956        let version = Version(0);
12957
12958        // Test: Single point should error
12959        let initial_source_point = "\
12960sketch(on = XY) {
12961  point(at = [var 1, var 2])
12962}
12963";
12964        let program_point = Program::parse(initial_source_point).unwrap().0.unwrap();
12965        let mut frontend_point = FrontendState::new();
12966        frontend_point.hack_set_program(&ctx, program_point).await.unwrap();
12967        let sketch_object_point = find_first_sketch_object(&frontend_point.scene_graph).unwrap();
12968        let sketch_id_point = sketch_object_point.id;
12969        let sketch_point = expect_sketch(sketch_object_point);
12970        let point_id = *sketch_point.segments.first().unwrap();
12971
12972        let constraint_point = Constraint::Diameter(Diameter {
12973            arc: point_id,
12974            diameter: Number {
12975                value: 10.0,
12976                units: NumericSuffix::Mm,
12977            },
12978            label_position: None,
12979            source: Default::default(),
12980        });
12981        let result_point = frontend_point
12982            .add_constraint(&mock_ctx, version, sketch_id_point, constraint_point)
12983            .await;
12984        assert!(result_point.is_err(), "Single point should error for diameter");
12985
12986        // Test: Single line segment should error (only arc segments supported)
12987        let initial_source_line = "\
12988sketch(on = XY) {
12989  line(start = [var 1, var 2], end = [var 3, var 4])
12990}
12991";
12992        let program_line = Program::parse(initial_source_line).unwrap().0.unwrap();
12993        let mut frontend_line = FrontendState::new();
12994        frontend_line.hack_set_program(&ctx, program_line).await.unwrap();
12995        let sketch_object_line = find_first_sketch_object(&frontend_line.scene_graph).unwrap();
12996        let sketch_id_line = sketch_object_line.id;
12997        let sketch_line = expect_sketch(sketch_object_line);
12998        let line_id = *sketch_line.segments.first().unwrap();
12999
13000        let constraint_line = Constraint::Diameter(Diameter {
13001            arc: line_id,
13002            diameter: Number {
13003                value: 10.0,
13004                units: NumericSuffix::Mm,
13005            },
13006            label_position: None,
13007            source: Default::default(),
13008        });
13009        let result_line = frontend_line
13010            .add_constraint(&mock_ctx, version, sketch_id_line, constraint_line)
13011            .await;
13012        assert!(result_line.is_err(), "Single line segment should error for diameter");
13013
13014        ctx.close().await;
13015        mock_ctx.close().await;
13016    }
13017
13018    #[tokio::test(flavor = "multi_thread")]
13019    async fn test_line_horizontal() {
13020        let initial_source = "\
13021sketch(on = XY) {
13022  line(start = [var 1, var 2], end = [var 3, var 4])
13023}
13024";
13025
13026        let program = Program::parse(initial_source).unwrap().0.unwrap();
13027
13028        let mut frontend = FrontendState::new();
13029
13030        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13031        let mock_ctx = ExecutorContext::new_mock(None).await;
13032        let version = Version(0);
13033
13034        frontend.hack_set_program(&ctx, program).await.unwrap();
13035        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13036        let sketch_id = sketch_object.id;
13037        let sketch = expect_sketch(sketch_object);
13038        let line1_id = *sketch.segments.get(2).unwrap();
13039
13040        let constraint = Constraint::Horizontal(Horizontal::Line { line: line1_id });
13041        let (src_delta, scene_delta) = frontend
13042            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13043            .await
13044            .unwrap();
13045        insta::assert_snapshot!("test_line_horizontal", src_delta.text.as_str());
13046        assert_eq!(
13047            scene_delta.new_graph.objects.len(),
13048            6,
13049            "{:#?}",
13050            scene_delta.new_graph.objects
13051        );
13052
13053        ctx.close().await;
13054        mock_ctx.close().await;
13055    }
13056
13057    #[tokio::test(flavor = "multi_thread")]
13058    async fn test_control_point_spline_edge_horizontal() {
13059        let initial_source = "\
13060@settings(experimentalFeatures = allow)
13061splineSketch = sketch(on = XY) {
13062  controlPointSpline1 = controlPointSpline(points = [
13063    [var 0mm, var 0mm],
13064    [var 10mm, var 20mm],
13065    [var 20mm, var 0mm],
13066  ])
13067}
13068";
13069
13070        let program = Program::parse(initial_source).unwrap().0.unwrap();
13071
13072        let mut frontend = FrontendState::new();
13073
13074        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13075        let mock_ctx = ExecutorContext::new_mock(None).await;
13076        let version = Version(0);
13077
13078        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13079        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13080        let sketch_id = sketch_object.id;
13081        let sketch = expect_sketch(sketch_object);
13082        let spline_id = sketch
13083            .segments
13084            .iter()
13085            .copied()
13086            .find(|seg_id| {
13087                matches!(
13088                    &frontend.scene_graph.objects[seg_id.0].kind,
13089                    ObjectKind::Segment {
13090                        segment: Segment::ControlPointSpline(_)
13091                    }
13092                )
13093            })
13094            .expect("Expected a control point spline segment in sketch");
13095        let edge_id = frontend
13096            .scene_graph
13097            .objects
13098            .iter()
13099            .find_map(|obj| match &obj.kind {
13100                ObjectKind::Segment {
13101                    segment: Segment::Line(line),
13102                } if line.owner == Some(spline_id) => Some(obj.id),
13103                _ => None,
13104            })
13105            .expect("Expected an owned control-polygon edge");
13106
13107        let constraint = Constraint::Horizontal(Horizontal::Line { line: edge_id });
13108        let (src_delta, _) = frontend
13109            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13110            .await
13111            .unwrap();
13112        assert!(
13113            src_delta.text.contains("horizontal(controlPointSpline1.edges[0])"),
13114            "Expected horizontal constraint on spline edge, got: {}",
13115            src_delta.text
13116        );
13117
13118        ctx.close().await;
13119        mock_ctx.close().await;
13120    }
13121
13122    #[tokio::test(flavor = "multi_thread")]
13123    async fn test_control_point_spline_edge_angle() {
13124        let initial_source = "\
13125@settings(experimentalFeatures = allow)
13126splineSketch = sketch(on = XY) {
13127  controlPointSpline1 = controlPointSpline(points = [
13128    [var 0mm, var 0mm],
13129    [var 10mm, var 20mm],
13130    [var 20mm, var 0mm],
13131  ])
13132
13133  line1 = line(start = [var 40mm, var 0mm], end = [var 60mm, var 10mm])
13134}
13135";
13136
13137        let program = Program::parse(initial_source).unwrap().0.unwrap();
13138
13139        let mut frontend = FrontendState::new();
13140
13141        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13142        let mock_ctx = ExecutorContext::new_mock(None).await;
13143        let version = Version(0);
13144
13145        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13146        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13147        let sketch_id = sketch_object.id;
13148        let sketch = expect_sketch(sketch_object);
13149        let spline_id = sketch
13150            .segments
13151            .iter()
13152            .copied()
13153            .find(|seg_id| {
13154                matches!(
13155                    &frontend.scene_graph.objects[seg_id.0].kind,
13156                    ObjectKind::Segment {
13157                        segment: Segment::ControlPointSpline(_)
13158                    }
13159                )
13160            })
13161            .expect("Expected a control point spline segment in sketch");
13162        let edge_id = frontend
13163            .scene_graph
13164            .objects
13165            .iter()
13166            .find_map(|obj| match &obj.kind {
13167                ObjectKind::Segment {
13168                    segment: Segment::Line(line),
13169                } if line.owner == Some(spline_id) => Some(obj.id),
13170                _ => None,
13171            })
13172            .expect("Expected an owned control-polygon edge");
13173        let line1_id = frontend
13174            .scene_graph
13175            .objects
13176            .iter()
13177            .find_map(|obj| match &obj.kind {
13178                ObjectKind::Segment {
13179                    segment: Segment::Line(line),
13180                } if line.owner.is_none() && obj.label == "line1" => Some(obj.id),
13181                _ => None,
13182            })
13183            .or_else(|| {
13184                sketch.segments.iter().copied().find(|seg_id| {
13185                    matches!(
13186                        &frontend.scene_graph.objects[seg_id.0].kind,
13187                        ObjectKind::Segment {
13188                            segment: Segment::Line(line),
13189                        } if line.owner.is_none()
13190                    )
13191                })
13192            })
13193            .expect("Expected a standalone line segment in sketch");
13194
13195        let constraint = Constraint::Angle(Angle {
13196            lines: vec![line1_id, edge_id],
13197            angle: Number {
13198                value: 30.0,
13199                units: NumericSuffix::Deg,
13200            },
13201            sector: None,
13202            inverse: None,
13203            label_position: None,
13204            source: Default::default(),
13205        });
13206        let (src_delta, _) = frontend
13207            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13208            .await
13209            .unwrap();
13210        assert!(
13211            src_delta
13212                .text
13213                .contains("angle([line1, controlPointSpline1.edges[0]]) == 30deg"),
13214            "Expected angle constraint on spline edge, got: {}",
13215            src_delta.text
13216        );
13217
13218        ctx.close().await;
13219        mock_ctx.close().await;
13220    }
13221
13222    #[tokio::test(flavor = "multi_thread")]
13223    async fn test_ui_scene_graph_hides_same_spline_coincident_constraints() {
13224        let initial_source = "\
13225@settings(experimentalFeatures = allow)
13226splineSketch = sketch(on = XY) {
13227  spline1 = controlPointSpline(points = [
13228    [var 0mm, var 0mm],
13229    [var 10mm, var 20mm],
13230    [var 20mm, var 0mm],
13231  ])
13232  line1 = line(start = [var 0mm, var 0mm], end = [var -10mm, var 0mm])
13233  coincident([spline1.controls[1], spline1.edges[0]])
13234  coincident([spline1.controls[0], line1])
13235}
13236";
13237
13238        let program = Program::parse(initial_source).unwrap().0.unwrap();
13239
13240        let mut frontend = FrontendState::new();
13241
13242        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13243        let mock_ctx = ExecutorContext::new_mock(None).await;
13244
13245        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13246
13247        let ui_scene_graph = frontend.scene_graph_for_ui();
13248        let sketch_object = find_first_sketch_object(&ui_scene_graph).unwrap();
13249        let sketch = expect_sketch(sketch_object);
13250
13251        assert_eq!(
13252            sketch.constraints.len(),
13253            1,
13254            "Expected only the external coincident constraint to remain visible in the UI scene graph"
13255        );
13256
13257        let visible_constraints = ui_scene_graph
13258            .objects
13259            .iter()
13260            .filter_map(|object| match &object.kind {
13261                ObjectKind::Constraint {
13262                    constraint: Constraint::Coincident(coincident),
13263                } => Some(coincident.clone()),
13264                _ => None,
13265            })
13266            .collect::<Vec<_>>();
13267
13268        assert_eq!(
13269            visible_constraints.len(),
13270            1,
13271            "Expected only one coincident constraint object in the UI scene graph"
13272        );
13273        assert_eq!(
13274            visible_constraints[0].get_segments().len(),
13275            2,
13276            "Expected the remaining visible coincident constraint to reference two segments"
13277        );
13278
13279        ctx.close().await;
13280        mock_ctx.close().await;
13281    }
13282
13283    #[tokio::test(flavor = "multi_thread")]
13284    async fn test_edit_control_point_spline_can_append_control_point() {
13285        let initial_source = "\
13286@settings(experimentalFeatures = allow)
13287splineSketch = sketch(on = XY) {
13288  controlPointSpline(points = [
13289    [var 0mm, var 0mm],
13290    [var 10mm, var 20mm],
13291    [var 20mm, var 0mm],
13292  ])
13293}
13294";
13295
13296        let program = Program::parse(initial_source).unwrap().0.unwrap();
13297
13298        let mut frontend = FrontendState::new();
13299
13300        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13301        let mock_ctx = ExecutorContext::new_mock(None).await;
13302        let version = Version(0);
13303
13304        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
13305        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13306        let sketch_id = sketch_object.id;
13307        let sketch = expect_sketch(sketch_object);
13308        let spline_id = sketch
13309            .segments
13310            .iter()
13311            .copied()
13312            .find(|seg_id| {
13313                matches!(
13314                    &frontend.scene_graph.objects[seg_id.0].kind,
13315                    ObjectKind::Segment {
13316                        segment: Segment::ControlPointSpline(_)
13317                    }
13318                )
13319            })
13320            .expect("Expected a control point spline segment in sketch");
13321
13322        let ctor = ControlPointSplineCtor {
13323            points: vec![
13324                Point2d {
13325                    x: Expr::Var(Number {
13326                        value: 0.0,
13327                        units: NumericSuffix::Mm,
13328                    }),
13329                    y: Expr::Var(Number {
13330                        value: 0.0,
13331                        units: NumericSuffix::Mm,
13332                    }),
13333                },
13334                Point2d {
13335                    x: Expr::Var(Number {
13336                        value: 10.0,
13337                        units: NumericSuffix::Mm,
13338                    }),
13339                    y: Expr::Var(Number {
13340                        value: 20.0,
13341                        units: NumericSuffix::Mm,
13342                    }),
13343                },
13344                Point2d {
13345                    x: Expr::Var(Number {
13346                        value: 20.0,
13347                        units: NumericSuffix::Mm,
13348                    }),
13349                    y: Expr::Var(Number {
13350                        value: 0.0,
13351                        units: NumericSuffix::Mm,
13352                    }),
13353                },
13354                Point2d {
13355                    x: Expr::Var(Number {
13356                        value: 30.0,
13357                        units: NumericSuffix::Mm,
13358                    }),
13359                    y: Expr::Var(Number {
13360                        value: 10.0,
13361                        units: NumericSuffix::Mm,
13362                    }),
13363                },
13364            ],
13365            construction: None,
13366        };
13367
13368        let segments = vec![ExistingSegmentCtor {
13369            id: spline_id,
13370            ctor: SegmentCtor::ControlPointSpline(ctor),
13371        }];
13372        let (src_delta, scene_delta) = frontend
13373            .edit_segments(&mock_ctx, version, sketch_id, segments)
13374            .await
13375            .unwrap();
13376
13377        assert!(
13378            src_delta.text.contains("[var 30mm, var 10mm]"),
13379            "Expected appended spline control point in source, got: {}",
13380            src_delta.text
13381        );
13382
13383        assert!(
13384            scene_delta.invalidates_ids,
13385            "Expected appending a spline control point to invalidate ids"
13386        );
13387        let updated_spline = scene_delta
13388            .new_graph
13389            .objects
13390            .iter()
13391            .find_map(|obj| match &obj.kind {
13392                ObjectKind::Segment {
13393                    segment: Segment::ControlPointSpline(updated_spline),
13394                } if updated_spline.controls.len() == 4 => Some(updated_spline),
13395                _ => None,
13396            })
13397            .expect("Expected edited scene graph to contain a four-point control point spline");
13398        assert_eq!(
13399            updated_spline.controls.len(),
13400            4,
13401            "Expected edited spline to expose four control points"
13402        );
13403
13404        ctx.close().await;
13405        mock_ctx.close().await;
13406    }
13407
13408    #[tokio::test(flavor = "multi_thread")]
13409    async fn test_line_vertical() {
13410        let initial_source = "\
13411sketch(on = XY) {
13412  line(start = [var 1, var 2], end = [var 3, var 4])
13413}
13414";
13415
13416        let program = Program::parse(initial_source).unwrap().0.unwrap();
13417
13418        let mut frontend = FrontendState::new();
13419
13420        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13421        let mock_ctx = ExecutorContext::new_mock(None).await;
13422        let version = Version(0);
13423
13424        frontend.hack_set_program(&ctx, program).await.unwrap();
13425        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13426        let sketch_id = sketch_object.id;
13427        let sketch = expect_sketch(sketch_object);
13428        let line1_id = *sketch.segments.get(2).unwrap();
13429
13430        let constraint = Constraint::Vertical(Vertical::Line { line: line1_id });
13431        let (src_delta, scene_delta) = frontend
13432            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13433            .await
13434            .unwrap();
13435        insta::assert_snapshot!("test_line_vertical", src_delta.text.as_str());
13436        assert_eq!(
13437            scene_delta.new_graph.objects.len(),
13438            6,
13439            "{:#?}",
13440            scene_delta.new_graph.objects
13441        );
13442
13443        ctx.close().await;
13444        mock_ctx.close().await;
13445    }
13446
13447    #[tokio::test(flavor = "multi_thread")]
13448    async fn test_points_vertical() {
13449        let initial_source = "\
13450sketch001 = sketch(on = XY) {
13451  p0 = point(at = [var -2.23mm, var 3.1mm])
13452  pf = point(at = [4, 4])
13453}
13454";
13455
13456        let program = Program::parse(initial_source).unwrap().0.unwrap();
13457
13458        let mut frontend = FrontendState::new();
13459
13460        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13461        let mock_ctx = ExecutorContext::new_mock(None).await;
13462        let version = Version(0);
13463
13464        frontend.hack_set_program(&ctx, program).await.unwrap();
13465        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13466        let sketch_id = sketch_object.id;
13467        let sketch = expect_sketch(sketch_object);
13468        let point_ids = vec![
13469            sketch.segments.first().unwrap().to_owned(),
13470            sketch.segments.get(1).unwrap().to_owned(),
13471        ];
13472
13473        let constraint = Constraint::Vertical(Vertical::Points {
13474            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13475        });
13476        let (src_delta, scene_delta) = frontend
13477            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13478            .await
13479            .unwrap();
13480        insta::assert_snapshot!("test_points_vertical", src_delta.text.as_str());
13481        assert_eq!(
13482            scene_delta.new_graph.objects.len(),
13483            5,
13484            "{:#?}",
13485            scene_delta.new_graph.objects
13486        );
13487
13488        ctx.close().await;
13489        mock_ctx.close().await;
13490    }
13491
13492    #[tokio::test(flavor = "multi_thread")]
13493    async fn test_points_horizontal() {
13494        let initial_source = "\
13495sketch001 = sketch(on = XY) {
13496  p0 = point(at = [var -2.23mm, var 3.1mm])
13497  pf = point(at = [4, 4])
13498}
13499";
13500
13501        let program = Program::parse(initial_source).unwrap().0.unwrap();
13502
13503        let mut frontend = FrontendState::new();
13504
13505        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13506        let mock_ctx = ExecutorContext::new_mock(None).await;
13507        let version = Version(0);
13508
13509        frontend.hack_set_program(&ctx, program).await.unwrap();
13510        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13511        let sketch_id = sketch_object.id;
13512        let sketch = expect_sketch(sketch_object);
13513        let point_ids = vec![
13514            sketch.segments.first().unwrap().to_owned(),
13515            sketch.segments.get(1).unwrap().to_owned(),
13516        ];
13517
13518        let constraint = Constraint::Horizontal(Horizontal::Points {
13519            points: point_ids.into_iter().map(ConstraintSegment::from).collect(),
13520        });
13521        let (src_delta, scene_delta) = frontend
13522            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13523            .await
13524            .unwrap();
13525        insta::assert_snapshot!("test_points_horizontal", src_delta.text.as_str());
13526        assert_eq!(
13527            scene_delta.new_graph.objects.len(),
13528            5,
13529            "{:#?}",
13530            scene_delta.new_graph.objects
13531        );
13532
13533        ctx.close().await;
13534        mock_ctx.close().await;
13535    }
13536
13537    #[tokio::test(flavor = "multi_thread")]
13538    async fn test_point_horizontal_with_origin() {
13539        let initial_source = "\
13540sketch001 = sketch(on = XY) {
13541  p0 = point(at = [var -2.23mm, var 3.1mm])
13542}
13543";
13544
13545        let program = Program::parse(initial_source).unwrap().0.unwrap();
13546
13547        let mut frontend = FrontendState::new();
13548
13549        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13550        let mock_ctx = ExecutorContext::new_mock(None).await;
13551        let version = Version(0);
13552
13553        frontend.hack_set_program(&ctx, program).await.unwrap();
13554        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13555        let sketch_id = sketch_object.id;
13556        let sketch = expect_sketch(sketch_object);
13557        let point_id = *sketch.segments.first().unwrap();
13558
13559        let constraint = Constraint::Horizontal(Horizontal::Points {
13560            points: vec![ConstraintSegment::from(point_id), ConstraintSegment::ORIGIN],
13561        });
13562        let (src_delta, scene_delta) = frontend
13563            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13564            .await
13565            .unwrap();
13566        insta::assert_snapshot!("test_point_horizontal_with_origin", src_delta.text.as_str());
13567        assert_eq!(
13568            scene_delta.new_graph.objects.len(),
13569            4,
13570            "{:#?}",
13571            scene_delta.new_graph.objects
13572        );
13573
13574        ctx.close().await;
13575        mock_ctx.close().await;
13576    }
13577
13578    #[tokio::test(flavor = "multi_thread")]
13579    async fn test_lines_equal_length() {
13580        let initial_source = "\
13581sketch(on = XY) {
13582  line(start = [var 1, var 2], end = [var 3, var 4])
13583  line(start = [var 5, var 6], end = [var 7, var 8])
13584}
13585";
13586
13587        let program = Program::parse(initial_source).unwrap().0.unwrap();
13588
13589        let mut frontend = FrontendState::new();
13590
13591        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13592        let mock_ctx = ExecutorContext::new_mock(None).await;
13593        let version = Version(0);
13594
13595        frontend.hack_set_program(&ctx, program).await.unwrap();
13596        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13597        let sketch_id = sketch_object.id;
13598        let sketch = expect_sketch(sketch_object);
13599        let line1_id = *sketch.segments.get(2).unwrap();
13600        let line2_id = *sketch.segments.get(5).unwrap();
13601
13602        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13603            lines: vec![line1_id, line2_id],
13604        });
13605        let (src_delta, scene_delta) = frontend
13606            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13607            .await
13608            .unwrap();
13609        insta::assert_snapshot!("test_lines_equal_length", src_delta.text.as_str());
13610        assert_eq!(
13611            scene_delta.new_graph.objects.len(),
13612            9,
13613            "{:#?}",
13614            scene_delta.new_graph.objects
13615        );
13616
13617        ctx.close().await;
13618        mock_ctx.close().await;
13619    }
13620
13621    #[tokio::test(flavor = "multi_thread")]
13622    async fn test_add_constraint_multi_line_equal_length() {
13623        let initial_source = "\
13624sketch(on = XY) {
13625  line(start = [var 1, var 2], end = [var 3, var 4])
13626  line(start = [var 5, var 6], end = [var 7, var 8])
13627  line(start = [var 9, var 10], end = [var 11, var 12])
13628}
13629";
13630
13631        let program = Program::parse(initial_source).unwrap().0.unwrap();
13632
13633        let mut frontend = FrontendState::new();
13634        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13635        let mock_ctx = ExecutorContext::new_mock(None).await;
13636        let version = Version(0);
13637
13638        frontend.hack_set_program(&ctx, program).await.unwrap();
13639        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13640        let sketch_id = sketch_object.id;
13641        let sketch = expect_sketch(sketch_object);
13642        let line1_id = *sketch.segments.get(2).unwrap();
13643        let line2_id = *sketch.segments.get(5).unwrap();
13644        let line3_id = *sketch.segments.get(8).unwrap();
13645
13646        let constraint = Constraint::LinesEqualLength(LinesEqualLength {
13647            lines: vec![line1_id, line2_id, line3_id],
13648        });
13649        let (src_delta, scene_delta) = frontend
13650            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13651            .await
13652            .unwrap();
13653        insta::assert_snapshot!("test_add_constraint_multi_line_equal_length", src_delta.text.as_str());
13654        let constraints = scene_delta
13655            .new_graph
13656            .objects
13657            .iter()
13658            .filter_map(|obj| {
13659                let ObjectKind::Constraint { constraint } = &obj.kind else {
13660                    return None;
13661                };
13662                Some(constraint)
13663            })
13664            .collect::<Vec<_>>();
13665
13666        assert_eq!(constraints.len(), 1, "{:#?}", frontend.scene_graph.objects);
13667        let Constraint::LinesEqualLength(lines_equal_length) = constraints[0] else {
13668            panic!("expected equal length constraint, got {:?}", constraints[0]);
13669        };
13670        assert_eq!(lines_equal_length.lines.len(), 3);
13671
13672        ctx.close().await;
13673        mock_ctx.close().await;
13674    }
13675
13676    #[tokio::test(flavor = "multi_thread")]
13677    async fn test_lines_parallel() {
13678        let initial_source = "\
13679sketch(on = XY) {
13680  line(start = [var 1, var 2], end = [var 3, var 4])
13681  line(start = [var 5, var 6], end = [var 7, var 8])
13682}
13683";
13684
13685        let program = Program::parse(initial_source).unwrap().0.unwrap();
13686
13687        let mut frontend = FrontendState::new();
13688
13689        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13690        let mock_ctx = ExecutorContext::new_mock(None).await;
13691        let version = Version(0);
13692
13693        frontend.hack_set_program(&ctx, program).await.unwrap();
13694        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13695        let sketch_id = sketch_object.id;
13696        let sketch = expect_sketch(sketch_object);
13697        let line1_id = *sketch.segments.get(2).unwrap();
13698        let line2_id = *sketch.segments.get(5).unwrap();
13699
13700        let constraint = Constraint::Parallel(Parallel {
13701            lines: vec![line1_id, line2_id],
13702        });
13703        let (src_delta, scene_delta) = frontend
13704            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13705            .await
13706            .unwrap();
13707        insta::assert_snapshot!("test_lines_parallel", src_delta.text.as_str());
13708        assert_eq!(
13709            scene_delta.new_graph.objects.len(),
13710            9,
13711            "{:#?}",
13712            scene_delta.new_graph.objects
13713        );
13714
13715        ctx.close().await;
13716        mock_ctx.close().await;
13717    }
13718
13719    #[tokio::test(flavor = "multi_thread")]
13720    async fn test_lines_parallel_multiline() {
13721        let initial_source = "\
13722sketch(on = XY) {
13723  line(start = [var 1, var 2], end = [var 3, var 4])
13724  line(start = [var 5, var 6], end = [var 7, var 8])
13725  line(start = [var 9, var 10], end = [var 11, var 12])
13726}
13727";
13728
13729        let program = Program::parse(initial_source).unwrap().0.unwrap();
13730
13731        let mut frontend = FrontendState::new();
13732
13733        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13734        let mock_ctx = ExecutorContext::new_mock(None).await;
13735        let version = Version(0);
13736
13737        frontend.hack_set_program(&ctx, program).await.unwrap();
13738        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13739        let sketch_id = sketch_object.id;
13740        let sketch = expect_sketch(sketch_object);
13741        let line1_id = *sketch.segments.get(2).unwrap();
13742        let line2_id = *sketch.segments.get(5).unwrap();
13743        let line3_id = *sketch.segments.get(8).unwrap();
13744
13745        let constraint = Constraint::Parallel(Parallel {
13746            lines: vec![line1_id, line2_id, line3_id],
13747        });
13748        let (src_delta, scene_delta) = frontend
13749            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13750            .await
13751            .unwrap();
13752        insta::assert_snapshot!("test_lines_parallel_multiline", src_delta.text.as_str());
13753
13754        let sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
13755        let sketch = expect_sketch(sketch_object);
13756        assert_eq!(sketch.constraints.len(), 1);
13757
13758        let constraint_object = scene_delta.new_graph.objects.get(sketch.constraints[0].0).unwrap();
13759        let ObjectKind::Constraint { constraint } = &constraint_object.kind else {
13760            panic!("Expected constraint object");
13761        };
13762        let Constraint::Parallel(parallel) = constraint else {
13763            panic!("Expected parallel constraint");
13764        };
13765        assert_eq!(parallel.lines.len(), 3);
13766
13767        ctx.close().await;
13768        mock_ctx.close().await;
13769    }
13770
13771    #[tokio::test(flavor = "multi_thread")]
13772    async fn test_lines_perpendicular() {
13773        let initial_source = "\
13774sketch(on = XY) {
13775  line(start = [var 1, var 2], end = [var 3, var 4])
13776  line(start = [var 5, var 6], end = [var 7, var 8])
13777}
13778";
13779
13780        let program = Program::parse(initial_source).unwrap().0.unwrap();
13781
13782        let mut frontend = FrontendState::new();
13783
13784        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13785        let mock_ctx = ExecutorContext::new_mock(None).await;
13786        let version = Version(0);
13787
13788        frontend.hack_set_program(&ctx, program).await.unwrap();
13789        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13790        let sketch_id = sketch_object.id;
13791        let sketch = expect_sketch(sketch_object);
13792        let line1_id = *sketch.segments.get(2).unwrap();
13793        let line2_id = *sketch.segments.get(5).unwrap();
13794
13795        let constraint = Constraint::Perpendicular(Perpendicular {
13796            lines: vec![line1_id, line2_id],
13797        });
13798        let (src_delta, scene_delta) = frontend
13799            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13800            .await
13801            .unwrap();
13802        insta::assert_snapshot!("test_lines_perpendicular", src_delta.text.as_str());
13803        assert_eq!(
13804            scene_delta.new_graph.objects.len(),
13805            9,
13806            "{:#?}",
13807            scene_delta.new_graph.objects
13808        );
13809
13810        ctx.close().await;
13811        mock_ctx.close().await;
13812    }
13813
13814    #[tokio::test(flavor = "multi_thread")]
13815    async fn test_lines_angle() {
13816        let initial_source = "\
13817sketch(on = XY) {
13818  line(start = [var 1, var 2], end = [var 3, var 4])
13819  line(start = [var 5, var 6], end = [var 7, var 8])
13820}
13821";
13822
13823        let program = Program::parse(initial_source).unwrap().0.unwrap();
13824
13825        let mut frontend = FrontendState::new();
13826
13827        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13828        let mock_ctx = ExecutorContext::new_mock(None).await;
13829        let version = Version(0);
13830
13831        frontend.hack_set_program(&ctx, program).await.unwrap();
13832        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13833        let sketch_id = sketch_object.id;
13834        let sketch = expect_sketch(sketch_object);
13835        let line1_id = *sketch.segments.get(2).unwrap();
13836        let line2_id = *sketch.segments.get(5).unwrap();
13837
13838        let constraint = Constraint::Angle(Angle {
13839            lines: vec![line1_id, line2_id],
13840            angle: Number {
13841                value: 30.0,
13842                units: NumericSuffix::Deg,
13843            },
13844            sector: None,
13845            inverse: None,
13846            label_position: None,
13847            source: Default::default(),
13848        });
13849        let (src_delta, scene_delta) = frontend
13850            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13851            .await
13852            .unwrap();
13853        insta::assert_snapshot!("test_lines_angle", src_delta.text.as_str());
13854        assert_eq!(
13855            scene_delta.new_graph.objects.len(),
13856            9,
13857            "{:#?}",
13858            scene_delta.new_graph.objects
13859        );
13860
13861        ctx.close().await;
13862        mock_ctx.close().await;
13863    }
13864
13865    #[tokio::test(flavor = "multi_thread")]
13866    async fn test_lines_angle_with_sector_uses_angle_dimension() {
13867        let initial_source = "\
13868sketch(on = XY) {
13869  line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13870  line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13871}
13872";
13873
13874        let program = Program::parse(initial_source).unwrap().0.unwrap();
13875
13876        let mut frontend = FrontendState::new();
13877
13878        let mock_ctx = ExecutorContext::new_mock(None).await;
13879        let version = Version(0);
13880
13881        frontend.program = program.clone();
13882        let outcome = mock_ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13883        frontend.update_state_after_exec(outcome, true);
13884        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13885        let sketch_id = sketch_object.id;
13886        let sketch = expect_sketch(sketch_object);
13887        let line1_id = *sketch.segments.get(2).unwrap();
13888        let line2_id = *sketch.segments.get(5).unwrap();
13889
13890        let constraint = Constraint::Angle(Angle {
13891            lines: vec![line1_id, line2_id],
13892            angle: Number {
13893                value: 270.0,
13894                units: NumericSuffix::Deg,
13895            },
13896            sector: Some(1),
13897            inverse: Some(true),
13898            label_position: Some(Point2d {
13899                x: Number {
13900                    value: -0.73,
13901                    units: NumericSuffix::Mm,
13902                },
13903                y: Number {
13904                    value: 0.75,
13905                    units: NumericSuffix::Mm,
13906                },
13907            }),
13908            source: Default::default(),
13909        });
13910        let (src_delta, _) = frontend
13911            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13912            .await
13913            .unwrap();
13914        assert_eq!(
13915            src_delta.text.as_str(),
13916            "\
13917sketch(on = XY) {
13918  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
13919  line2 = line(start = [var 0mm, var 0mm], end = [var 0mm, var 4mm])
13920  angleDimension(
13921    lines = [line1, line2],
13922    sector = 1,
13923    inverse = true,
13924    labelPosition = [-0.73mm, 0.75mm],
13925  ) == 270deg
13926}
13927"
13928        );
13929
13930        mock_ctx.close().await;
13931    }
13932
13933    #[tokio::test(flavor = "multi_thread")]
13934    async fn test_segments_tangent() {
13935        let initial_source = "\
13936sketch(on = XY) {
13937  line(start = [var 1, var 2], end = [var 3, var 4])
13938  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
13939}
13940";
13941
13942        let program = Program::parse(initial_source).unwrap().0.unwrap();
13943
13944        let mut frontend = FrontendState::new();
13945
13946        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
13947        let mock_ctx = ExecutorContext::new_mock(None).await;
13948        let version = Version(0);
13949
13950        frontend.hack_set_program(&ctx, program).await.unwrap();
13951        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13952        let sketch_id = sketch_object.id;
13953        let sketch = expect_sketch(sketch_object);
13954        let line1_id = *sketch.segments.get(2).unwrap();
13955        let arc1_id = *sketch.segments.get(6).unwrap();
13956
13957        let constraint = Constraint::Tangent(Tangent {
13958            input: vec![line1_id, arc1_id],
13959        });
13960        let (src_delta, scene_delta) = frontend
13961            .add_constraint(&mock_ctx, version, sketch_id, constraint)
13962            .await
13963            .unwrap();
13964        insta::assert_snapshot!("test_segments_tangent", src_delta.text.as_str());
13965        assert_eq!(
13966            scene_delta.new_graph.objects.len(),
13967            10,
13968            "{:#?}",
13969            scene_delta.new_graph.objects
13970        );
13971
13972        ctx.close().await;
13973        mock_ctx.close().await;
13974    }
13975
13976    #[tokio::test(flavor = "multi_thread")]
13977    async fn test_point_midpoint() {
13978        let initial_source = "\
13979sketch(on = XY) {
13980  point(at = [var 1, var 1])
13981  line(start = [var 0, var 0], end = [var 6, var 4])
13982}
13983";
13984
13985        let program = Program::parse(initial_source).unwrap().0.unwrap();
13986
13987        let mut frontend = FrontendState::new();
13988
13989        let ctx = ExecutorContext::new_mock(None).await;
13990        let version = Version(0);
13991
13992        frontend.program = program.clone();
13993        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
13994        frontend.update_state_after_exec(outcome, true);
13995        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
13996        let sketch_id = sketch_object.id;
13997        let sketch = expect_sketch(sketch_object);
13998        let point_id = *sketch.segments.first().unwrap();
13999        let line_id = *sketch.segments.get(3).unwrap();
14000
14001        let constraint = Constraint::Midpoint(Midpoint {
14002            point: ConstraintSegment::from(point_id),
14003            segment: line_id,
14004        });
14005        let (src_delta, scene_delta) = frontend
14006            .add_constraint(&ctx, version, sketch_id, constraint)
14007            .await
14008            .unwrap();
14009        insta::assert_snapshot!("test_point_midpoint", src_delta.text.as_str());
14010        assert_eq!(
14011            scene_delta.new_graph.objects.len(),
14012            7,
14013            "{:#?}",
14014            scene_delta.new_graph.objects
14015        );
14016
14017        ctx.close().await;
14018    }
14019
14020    #[tokio::test(flavor = "multi_thread")]
14021    async fn test_segments_symmetric() {
14022        let initial_source = "\
14023sketch(on = XY) {
14024  line(start = [var 0, var 0], end = [var 0, var 4])
14025  line(start = [var 4, var 0], end = [var 4, var 4])
14026  line(start = [var 2, var -1], end = [var 2, var 5])
14027}
14028";
14029
14030        let program = Program::parse(initial_source).unwrap().0.unwrap();
14031
14032        let mut frontend = FrontendState::new();
14033
14034        let ctx = ExecutorContext::new_mock(None).await;
14035        let version = Version(0);
14036
14037        frontend.program = program.clone();
14038        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14039        frontend.update_state_after_exec(outcome, true);
14040        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14041        let sketch_id = sketch_object.id;
14042        let sketch = expect_sketch(sketch_object);
14043        let line1_id = *sketch.segments.get(2).unwrap();
14044        let line2_id = *sketch.segments.get(5).unwrap();
14045        let axis_id = *sketch.segments.get(8).unwrap();
14046
14047        let constraint = Constraint::Symmetric(Symmetric {
14048            input: vec![line1_id, line2_id],
14049            axis: axis_id,
14050        });
14051        let (src_delta, scene_delta) = frontend
14052            .add_constraint(&ctx, version, sketch_id, constraint)
14053            .await
14054            .unwrap();
14055        insta::assert_snapshot!("test_segments_symmetric", src_delta.text.as_str());
14056        assert_eq!(
14057            scene_delta.new_graph.objects.len(),
14058            12,
14059            "{:#?}",
14060            scene_delta.new_graph.objects
14061        );
14062
14063        ctx.close().await;
14064    }
14065
14066    #[tokio::test(flavor = "multi_thread")]
14067    async fn test_point_arc_midpoint() {
14068        let initial_source = "\
14069sketch(on = XY) {
14070  point(at = [var 6, var 3])
14071  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14072}
14073";
14074
14075        let program = Program::parse(initial_source).unwrap().0.unwrap();
14076
14077        let mut frontend = FrontendState::new();
14078
14079        let ctx = ExecutorContext::new_mock(None).await;
14080        let version = Version(0);
14081
14082        frontend.program = program.clone();
14083        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14084        frontend.update_state_after_exec(outcome, true);
14085        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14086        let sketch_id = sketch_object.id;
14087        let sketch = expect_sketch(sketch_object);
14088        let point_id = *sketch.segments.first().unwrap();
14089        let arc_id = *sketch.segments.get(4).unwrap();
14090
14091        let constraint = Constraint::Midpoint(Midpoint {
14092            point: ConstraintSegment::from(point_id),
14093            segment: arc_id,
14094        });
14095        let (src_delta, scene_delta) = frontend
14096            .add_constraint(&ctx, version, sketch_id, constraint)
14097            .await
14098            .unwrap();
14099        insta::assert_snapshot!("test_point_arc_midpoint", src_delta.text.as_str());
14100        assert_eq!(
14101            scene_delta.new_graph.objects.len(),
14102            8,
14103            "{:#?}",
14104            scene_delta.new_graph.objects
14105        );
14106
14107        ctx.close().await;
14108    }
14109
14110    #[tokio::test(flavor = "multi_thread")]
14111    async fn test_origin_line_midpoint() {
14112        let initial_source = "\
14113sketch(on = XY) {
14114  line(start = [var 0, var 0], end = [var 6, var 4])
14115}
14116";
14117
14118        let program = Program::parse(initial_source).unwrap().0.unwrap();
14119
14120        let mut frontend = FrontendState::new();
14121
14122        let ctx = ExecutorContext::new_mock(None).await;
14123        let version = Version(0);
14124
14125        frontend.program = program.clone();
14126        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14127        frontend.update_state_after_exec(outcome, true);
14128        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14129        let sketch_id = sketch_object.id;
14130        let sketch = expect_sketch(sketch_object);
14131        let line_id = *sketch.segments.get(2).unwrap();
14132
14133        let constraint = Constraint::Midpoint(Midpoint {
14134            point: ConstraintSegment::ORIGIN,
14135            segment: line_id,
14136        });
14137        let (src_delta, scene_delta) = frontend
14138            .add_constraint(&ctx, version, sketch_id, constraint)
14139            .await
14140            .unwrap();
14141        insta::assert_snapshot!("test_origin_line_midpoint", src_delta.text.as_str());
14142        assert_eq!(
14143            scene_delta.new_graph.objects.len(),
14144            6,
14145            "{:#?}",
14146            scene_delta.new_graph.objects
14147        );
14148
14149        ctx.close().await;
14150    }
14151
14152    #[tokio::test(flavor = "multi_thread")]
14153    async fn test_origin_arc_midpoint() {
14154        let initial_source = "\
14155sketch(on = XY) {
14156  arc(start = [var 5, var 2], end = [var 7, var 2], center = [var 6, var 2])
14157}
14158";
14159
14160        let program = Program::parse(initial_source).unwrap().0.unwrap();
14161
14162        let mut frontend = FrontendState::new();
14163
14164        let ctx = ExecutorContext::new_mock(None).await;
14165        let version = Version(0);
14166
14167        frontend.program = program.clone();
14168        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14169        frontend.update_state_after_exec(outcome, true);
14170        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14171        let sketch_id = sketch_object.id;
14172        let sketch = expect_sketch(sketch_object);
14173        let arc_id = *sketch.segments.get(3).unwrap();
14174
14175        let constraint = Constraint::Midpoint(Midpoint {
14176            point: ConstraintSegment::ORIGIN,
14177            segment: arc_id,
14178        });
14179        let (src_delta, scene_delta) = frontend
14180            .add_constraint(&ctx, version, sketch_id, constraint)
14181            .await
14182            .unwrap();
14183        insta::assert_snapshot!("test_origin_arc_midpoint", src_delta.text.as_str());
14184        assert_eq!(
14185            scene_delta.new_graph.objects.len(),
14186            7,
14187            "{:#?}",
14188            scene_delta.new_graph.objects
14189        );
14190
14191        ctx.close().await;
14192    }
14193
14194    #[tokio::test(flavor = "multi_thread")]
14195    async fn test_segments_symmetric_arcs() {
14196        let initial_source = "\
14197sketch(on = XY) {
14198  arc(start = [var -15, var 0], end = [var -10, var 5], center = [var -10, var 0])
14199  arc(start = [var 6, var 2], end = [var 12, var -4], center = [var 8, var 1])
14200  line(start = [var 0, var -10], end = [var 0, var 10])
14201}
14202";
14203
14204        let program = Program::parse(initial_source).unwrap().0.unwrap();
14205
14206        let mut frontend = FrontendState::new();
14207
14208        let ctx = ExecutorContext::new_mock(None).await;
14209        let version = Version(0);
14210
14211        frontend.program = program.clone();
14212        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14213        frontend.update_state_after_exec(outcome, true);
14214        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
14215        let sketch_id = sketch_object.id;
14216        let sketch = expect_sketch(sketch_object);
14217        let arc1_id = *sketch.segments.get(3).unwrap();
14218        let arc2_id = *sketch.segments.get(7).unwrap();
14219        let axis_id = *sketch.segments.get(10).unwrap();
14220
14221        let constraint = Constraint::Symmetric(Symmetric {
14222            input: vec![arc1_id, arc2_id],
14223            axis: axis_id,
14224        });
14225        let (src_delta, scene_delta) = frontend
14226            .add_constraint(&ctx, version, sketch_id, constraint)
14227            .await
14228            .unwrap();
14229        insta::assert_snapshot!("test_segments_symmetric_arcs", src_delta.text.as_str());
14230        assert_eq!(
14231            scene_delta.new_graph.objects.len(),
14232            14,
14233            "{:#?}",
14234            scene_delta.new_graph.objects
14235        );
14236
14237        ctx.close().await;
14238    }
14239
14240    #[tokio::test(flavor = "multi_thread")]
14241    async fn test_sketch_on_face_simple() {
14242        let initial_source = "\
14243len = 2mm
14244cube = startSketchOn(XY)
14245  |> startProfile(at = [0, 0])
14246  |> line(end = [len, 0], tag = $side)
14247  |> line(end = [0, len])
14248  |> line(end = [-len, 0])
14249  |> line(end = [0, -len])
14250  |> close()
14251  |> extrude(length = len)
14252
14253face = faceOf(cube, face = side)
14254";
14255
14256        let program = Program::parse(initial_source).unwrap().0.unwrap();
14257
14258        let mut frontend = FrontendState::new();
14259
14260        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14261        let mock_ctx = ExecutorContext::new_mock(None).await;
14262        let version = Version(0);
14263
14264        frontend.hack_set_program(&ctx, program).await.unwrap();
14265        let face_object = find_first_face_object(&frontend.scene_graph).unwrap();
14266        let face_id = face_object.id;
14267
14268        let sketch_args = SketchCtor {
14269            on: Plane::Object(face_id),
14270        };
14271        let (_src_delta, scene_delta, sketch_id) = frontend
14272            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14273            .await
14274            .unwrap();
14275        assert_eq!(sketch_id, ObjectId(2));
14276        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14277        let sketch_object = &scene_delta.new_graph.objects[2];
14278        assert_eq!(sketch_object.id, ObjectId(2));
14279        assert_eq!(
14280            sketch_object.kind,
14281            ObjectKind::Sketch(Sketch {
14282                args: SketchCtor {
14283                    on: Plane::Object(face_id),
14284                },
14285                plane: face_id,
14286                segments: vec![],
14287                constraints: vec![],
14288            })
14289        );
14290        assert_eq!(scene_delta.new_graph.objects.len(), 8);
14291
14292        ctx.close().await;
14293        mock_ctx.close().await;
14294    }
14295
14296    #[tokio::test(flavor = "multi_thread")]
14297    async fn test_new_sketch_on_primitive_index_face() {
14298        let initial_source = "\
14299@settings(kclVersion = 2.0)
14300
14301sketch001 = sketch(on = XY) {
14302  circle1 = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
14303}
14304extrude001 = extrude(region(point = [0mm, 0mm], sketch = sketch001), length = 5, tagEnd = $capEnd001)
14305shell001 = shell(extrude001, faces = capEnd001, thickness = 1)";
14306        let program = Program::parse(initial_source).unwrap().0.unwrap();
14307        let ctx = ExecutorContext::new_mock(None).await;
14308        let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14309        let solid_id = match outcome.variables.get("shell001") {
14310            Some(KclValueView::Solid { value }) => value.id,
14311            value => panic!("expected shell001 to be a solid, got {value:?}"),
14312        };
14313        let solid_references = solid_references_from_variables(&program.ast, &outcome.variables);
14314
14315        let mut ast = program.ast;
14316        let scene_graph = SceneGraph::empty(ProjectId(0), FileId(0), Version(0));
14317        let face_expr = sketch_on_ast_expr(
14318            &mut ast,
14319            &scene_graph,
14320            &solid_references,
14321            &Plane::PrimitiveFace(kcl_api::PrimitiveFacePlane { solid_id, index: 6 }),
14322        )
14323        .unwrap();
14324        let face_decl = ast::VariableDeclaration::new(
14325            ast::VariableDeclarator::new("face001", face_expr),
14326            ast::ItemVisibility::Default,
14327            ast::VariableKind::Const,
14328        );
14329        ast.body
14330            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14331                face_decl,
14332            ))));
14333        let face_source = source_from_ast(&ast);
14334        let new_source = format!("{face_source}sketch002 = sketch(on = face001) {{\n}}\n");
14335        insta::assert_snapshot!("test_new_sketch_on_primitive_index_face", new_source);
14336
14337        let program = Program::parse(&new_source).unwrap().0.unwrap();
14338        ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
14339        ctx.close().await;
14340    }
14341
14342    #[tokio::test(flavor = "multi_thread")]
14343    async fn test_sketch_on_wall_artifact_from_region_extrude() {
14344        let initial_source = "\
14345s = sketch(on = YZ) {
14346  line1 = line(start = [0, 0], end = [0, 1])
14347  line2 = line(start = [0, 1], end = [1, 1])
14348  line3 = line(start = [1, 1], end = [0, 0])
14349}
14350region001 = region(point = [0.1, 0.1], sketch = s)
14351extrude001 = extrude(region001, length = 5)
14352";
14353
14354        let program = Program::parse(initial_source).unwrap().0.unwrap();
14355
14356        let mut frontend = FrontendState::new();
14357        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14358        let version = Version(0);
14359
14360        frontend.hack_set_program(&ctx, program).await.unwrap();
14361        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14362
14363        let sketch_args = SketchCtor {
14364            on: Plane::Object(wall_object_id),
14365        };
14366        let (src_delta, _scene_delta, _sketch_id) = frontend
14367            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14368            .await
14369            .unwrap();
14370        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14371
14372        ctx.close().await;
14373    }
14374
14375    #[tokio::test(flavor = "multi_thread")]
14376    async fn test_sketch_on_wall_artifact_from_split_region_extrude() {
14377        let initial_source = "\
14378sketch001 = sketch(on = YZ) {
14379  line1 = line(start = [var 0.49, var -0.39], end = [var 6.52, var -0.39])
14380  line2 = line(start = [var 6.52, var -0.39], end = [var 6.52, var 4.9])
14381  line3 = line(start = [var 6.52, var 4.9], end = [var 0.49, var 4.9])
14382  line4 = line(start = [var 0.49, var 4.9], end = [var 0.49, var -0.39])
14383  coincident([line1.end, line2.start])
14384  coincident([line2.end, line3.start])
14385  coincident([line3.end, line4.start])
14386  coincident([line4.end, line1.start])
14387  parallel([line2, line4])
14388  parallel([line3, line1])
14389  perpendicular([line1, line2])
14390  horizontal(line3)
14391  line5 = line(start = [2.35, 6.65], end = [5.89, -2.7])
14392}
14393region001 = region(point = [3.1, 3.74], sketch = sketch001)
14394extrude001 = extrude(region001, length = 5)
14395";
14396
14397        let program = Program::parse(initial_source).unwrap().0.unwrap();
14398
14399        let mut frontend = FrontendState::new();
14400        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14401        let version = Version(0);
14402
14403        frontend.hack_set_program(&ctx, program).await.unwrap();
14404        let wall_object_id = find_first_wall_object_id(&frontend.scene_graph).expect("expected a wall object");
14405
14406        let sketch_args = SketchCtor {
14407            on: Plane::Object(wall_object_id),
14408        };
14409        let (src_delta, _scene_delta, _sketch_id) = frontend
14410            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14411            .await
14412            .unwrap();
14413        assert!(src_delta.text.contains("faceOf(extrude001, face = region001.tags."));
14414
14415        ctx.close().await;
14416    }
14417
14418    #[tokio::test(flavor = "multi_thread")]
14419    async fn test_new_sketch_on_multi_region_extrude_cap_indexes_selected_solid() {
14420        let initial_source = "\
14421@settings(kclVersion = 2.0)
14422
14423sketch001 = sketch(on = XY) {
14424  circle1 = circle(start = [var -1.51mm, var 1.31mm], center = [var -1.98mm, var 1.03mm])
14425  circle2 = circle(start = [var 2.98mm, var 2.37mm], center = [var 2.66mm, var 1.46mm])
14426}
14427hidden001 = hide(sketch001)
14428region001 = region(segments = [sketch001.circle2])
14429region002 = region(segments = [sketch001.circle1])
14430extrude001 = extrude([region001, region002], length = 5)
14431";
14432
14433        let program = Program::parse(initial_source).unwrap().0.unwrap();
14434        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14435        let version = Version(0);
14436
14437        for (solid_output_index, expected_face) in [
14438            (0, "faceOf(extrude001[0], face = END)"),
14439            (1, "faceOf(extrude001[1], face = END)"),
14440        ] {
14441            let mut frontend = FrontendState::new();
14442            frontend.hack_set_program(&ctx, program.clone()).await.unwrap();
14443            let cap_object_id = find_cap_object_id_with_solid_output_index(
14444                &frontend.scene_graph,
14445                kcl_api::CapKind::End,
14446                solid_output_index,
14447            )
14448            .unwrap_or_else(|| panic!("expected an end cap object for solid output index {solid_output_index}"));
14449
14450            let sketch_args = SketchCtor {
14451                on: Plane::Object(cap_object_id),
14452            };
14453            let (src_delta, _scene_delta, _sketch_id) = frontend
14454                .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14455                .await
14456                .unwrap();
14457
14458            assert!(
14459                src_delta.text.contains(expected_face),
14460                "expected `{expected_face}` in:\n{}",
14461                src_delta.text
14462            );
14463            assert!(!src_delta.text.contains("faceOf(extrude001, face = END)"));
14464        }
14465
14466        ctx.close().await;
14467    }
14468
14469    #[tokio::test(flavor = "multi_thread")]
14470    async fn test_new_sketch_on_multi_region_extrude_wall_indexes_selected_solid() {
14471        let initial_source = "\
14472@settings(kclVersion = 2.0)
14473
14474sketch001 = sketch(on = XY) {
14475  rect1Line1 = line(start = [0, 0], end = [1, 0])
14476  rect1Line2 = line(start = [1, 0], end = [1, 1])
14477  rect1Line3 = line(start = [1, 1], end = [0, 1])
14478  rect1Line4 = line(start = [0, 1], end = [0, 0])
14479  rect2Line1 = line(start = [3, 0], end = [4, 0])
14480  rect2Line2 = line(start = [4, 0], end = [4, 1])
14481  rect2Line3 = line(start = [4, 1], end = [3, 1])
14482  rect2Line4 = line(start = [3, 1], end = [3, 0])
14483}
14484hidden001 = hide(sketch001)
14485region001 = region(segments = [
14486  sketch001.rect1Line4,
14487  sketch001.rect1Line1
14488])
14489region002 = region(segments = [
14490  sketch001.rect2Line4,
14491  sketch001.rect2Line1
14492])
14493extrude001 = extrude([region001, region002], length = 5)
14494";
14495
14496        let program = Program::parse(initial_source).unwrap().0.unwrap();
14497        let mut frontend = FrontendState::new();
14498        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14499        let version = Version(0);
14500
14501        frontend.hack_set_program(&ctx, program).await.unwrap();
14502        let region_call = "\
14503region(segments = [
14504  sketch001.rect1Line4,
14505  sketch001.rect1Line1
14506])";
14507        let region_call_start = initial_source.find(region_call).unwrap();
14508        let region_range = [region_call_start, region_call_start + region_call.len(), 0].into();
14509        let segment_call = "line(start = [0, 0], end = [1, 0])";
14510        let segment_call_start = initial_source.find(segment_call).unwrap();
14511        let segment_range = [segment_call_start, segment_call_start + segment_call.len(), 0].into();
14512        let wall_object_id = frontend
14513            .scene_graph
14514            .objects
14515            .iter()
14516            .find_map(|object| match &object.kind {
14517                ObjectKind::Wall(wall)
14518                    if wall.source.path.as_ref().is_some_and(|path| path.range == region_range)
14519                        && wall.source.segment.range == segment_range =>
14520                {
14521                    Some(object.id)
14522                }
14523                _ => None,
14524            })
14525            .expect("expected a wall object for region001.tags.rect1Line1");
14526
14527        let sketch_args = SketchCtor {
14528            on: Plane::Object(wall_object_id),
14529        };
14530        let (src_delta, _scene_delta, _sketch_id) = frontend
14531            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14532            .await
14533            .unwrap();
14534
14535        let expected_face = "faceOf(extrude001[0], face = region001.tags.rect1Line1)";
14536        assert!(
14537            src_delta.text.contains(expected_face),
14538            "expected `{expected_face}` in:\n{}",
14539            src_delta.text
14540        );
14541        assert!(!src_delta.text.contains("faceOf(extrude001, face ="));
14542
14543        ctx.close().await;
14544    }
14545
14546    #[test]
14547    fn test_enclosing_variable_fallback_skips_nested_sketch_items() {
14548        let source = "\
14549sketch001 = sketch(on = XY) {
14550  line(start = [0, 0], end = [1, 0])
14551}
14552part = subtract(boxSolid, tools = [cutSolid])
14553  |> appearance(color = \"#8f96a3\")
14554";
14555        let ast = Program::parse(source).unwrap().0.unwrap().ast;
14556        let line_start = source.find("line").unwrap();
14557        let line_end = line_start + "line(start = [0, 0], end = [1, 0])".len();
14558        let line_ref = SourceRef::Simple {
14559            range: [line_start, line_end, 0].into(),
14560            node_path: None,
14561        };
14562        assert_eq!(variable_name_containing_source_ref(&ast, &line_ref), None);
14563
14564        let subtract_start = source.find("subtract").unwrap();
14565        let subtract_end = subtract_start + "subtract(boxSolid, tools = [cutSolid])".len();
14566        let subtract_ref = SourceRef::Simple {
14567            range: [subtract_start, subtract_end, 0].into(),
14568            node_path: None,
14569        };
14570        assert_eq!(
14571            variable_name_containing_source_ref(&ast, &subtract_ref),
14572            Some("part".to_owned())
14573        );
14574    }
14575
14576    #[tokio::test(flavor = "multi_thread")]
14577    async fn test_sketch_on_subtracted_sweep_cap_uses_composite_solid() {
14578        clear_mem_cache().await;
14579        let source = "\
14580boxSolid = startSketchOn(XY)
14581  |> startProfile(at = [0, 0])
14582  |> line(end = [4, 0], tag = $bottomEdge)
14583  |> line(end = [0, 4])
14584  |> line(end = [-4, 0])
14585  |> close()
14586  |> extrude(length = 10)
14587cutSolid = startSketchOn(XY)
14588  |> startProfile(at = [1, 1])
14589  |> line(end = [1, 0])
14590  |> line(end = [0, 1])
14591  |> line(end = [-1, 0])
14592  |> close()
14593  |> extrude(length = 10)
14594part = subtract(boxSolid, tools = [cutSolid])
14595  |> appearance(color = \"#8f96a3\", roughness = 55, metalness = 8)
14596";
14597        let program = Program::parse(source).unwrap().0.unwrap();
14598        let mut frontend = FrontendState::new();
14599        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14600        match frontend.hack_set_program(&ctx, program).await.unwrap() {
14601            SetProgramOutcome::Success { .. } => {}
14602            SetProgramOutcome::ExecFailure { error } => panic!("KCL fixture failed to execute: {error:?}"),
14603        }
14604
14605        let sweep_call_start = source.find("extrude").unwrap();
14606        let sweep_call_end = sweep_call_start + "extrude(length = 10)".len();
14607        let part_call_start = source.find("subtract").unwrap();
14608        let part_call_end = part_call_start + "subtract(boxSolid, tools = [cutSolid])".len();
14609        let sweep_range = [sweep_call_start, sweep_call_end, 0].into();
14610        let composite_range = [part_call_start, part_call_end, 0].into();
14611
14612        let cap_object = frontend
14613            .scene_graph
14614            .objects
14615            .iter()
14616            .find(|object| {
14617                matches!(
14618                    &object.kind,
14619                    ObjectKind::Cap(kcl_api::Cap {
14620                        kind: kcl_api::CapKind::End,
14621                        source,
14622                        ..
14623                    }) if source.solid.range == composite_range && source.sweep.range == sweep_range
14624                )
14625            })
14626            .expect("expected end cap object to trace through subtract and original extrude");
14627
14628        let mut ast = frontend.program.ast.clone();
14629        let cap_expr = sketch_on_ast_expr(
14630            &mut ast,
14631            &frontend.scene_graph,
14632            &frontend.solid_references,
14633            &Plane::Object(cap_object.id),
14634        )
14635        .unwrap();
14636        let cap_face_decl = ast::VariableDeclaration::new(
14637            ast::VariableDeclarator::new("capFace", cap_expr.clone()),
14638            ast::ItemVisibility::Default,
14639            ast::VariableKind::Const,
14640        );
14641        ast.body
14642            .push(ast::BodyItem::VariableDeclaration(BoxNode::new(ast::Node::no_src(
14643                cap_face_decl,
14644            ))));
14645        let generated_source = source_from_ast(&ast);
14646
14647        assert!(generated_source.contains("capFace = faceOf(part, face = END)"));
14648        assert!(!generated_source.contains("faceOf(boxSolid"));
14649        let ast::Expr::CallExpressionKw(call) = cap_expr else {
14650            panic!("expected faceOf call");
14651        };
14652        assert_eq!(call.callee.name.name, "faceOf");
14653        let ast::Expr::Name(solid_name) = call.unlabeled.as_ref().unwrap() else {
14654            panic!("expected solid name");
14655        };
14656        assert_eq!(solid_name.name.name, "part");
14657        let ast::Expr::Name(face_name) = &call.arguments[0].arg else {
14658            panic!("expected face name");
14659        };
14660        assert_eq!(face_name.name.name, "END");
14661
14662        ctx.close().await;
14663    }
14664
14665    #[tokio::test(flavor = "multi_thread")]
14666    async fn test_sketch_on_plane_incremental() {
14667        let initial_source = "\
14668len = 2mm
14669cube = startSketchOn(XY)
14670  |> startProfile(at = [0, 0])
14671  |> line(end = [len, 0], tag = $side)
14672  |> line(end = [0, len])
14673  |> line(end = [-len, 0])
14674  |> line(end = [0, -len])
14675  |> close()
14676  |> extrude(length = len)
14677
14678plane = planeOf(cube, face = side)
14679";
14680
14681        let program = Program::parse(initial_source).unwrap().0.unwrap();
14682
14683        let mut frontend = FrontendState::new();
14684
14685        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14686        let mock_ctx = ExecutorContext::new_mock(None).await;
14687        let version = Version(0);
14688
14689        frontend.hack_set_program(&ctx, program).await.unwrap();
14690        // Find the last plane since the first plane is the XY plane.
14691        let plane_object = frontend
14692            .scene_graph
14693            .objects
14694            .iter()
14695            .rev()
14696            .find(|object| matches!(&object.kind, ObjectKind::Plane(_)))
14697            .unwrap();
14698        let plane_id = plane_object.id;
14699
14700        let sketch_args = SketchCtor {
14701            on: Plane::Object(plane_id),
14702        };
14703        let (src_delta, scene_delta, sketch_id) = frontend
14704            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14705            .await
14706            .unwrap();
14707        insta::assert_snapshot!("test_sketch_on_plane_incremental", src_delta.text.as_str());
14708        assert_eq!(sketch_id, ObjectId(2));
14709        assert_eq!(scene_delta.new_objects, vec![ObjectId(2)]);
14710        let sketch_object = &scene_delta.new_graph.objects[2];
14711        assert_eq!(sketch_object.id, ObjectId(2));
14712        assert_eq!(
14713            sketch_object.kind,
14714            ObjectKind::Sketch(Sketch {
14715                args: SketchCtor {
14716                    on: Plane::Object(plane_id),
14717                },
14718                plane: plane_id,
14719                segments: vec![],
14720                constraints: vec![],
14721            })
14722        );
14723        assert_eq!(scene_delta.new_graph.objects.len(), 9);
14724
14725        let plane_object = scene_delta.new_graph.objects.get(plane_id.0).unwrap();
14726        assert_eq!(plane_object.id, plane_id);
14727        assert_eq!(plane_object.kind, ObjectKind::Plane(Plane::Object(plane_id)));
14728
14729        ctx.close().await;
14730        mock_ctx.close().await;
14731    }
14732
14733    #[tokio::test(flavor = "multi_thread")]
14734    async fn test_new_sketch_uses_unique_variable_name() {
14735        let initial_source = "\
14736sketch1 = sketch(on = XY) {
14737}
14738";
14739
14740        let program = Program::parse(initial_source).unwrap().0.unwrap();
14741
14742        let mut frontend = FrontendState::new();
14743        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14744        let version = Version(0);
14745
14746        frontend.hack_set_program(&ctx, program).await.unwrap();
14747
14748        let sketch_args = SketchCtor {
14749            on: Plane::Default(PlaneName::Yz),
14750        };
14751        let (src_delta, _, _) = frontend
14752            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14753            .await
14754            .unwrap();
14755
14756        insta::assert_snapshot!("test_new_sketch_uses_unique_variable_name", src_delta.text.as_str());
14757
14758        ctx.close().await;
14759    }
14760
14761    #[tokio::test(flavor = "multi_thread")]
14762    async fn test_new_sketch_twice_using_same_plane() {
14763        let initial_source = "\
14764sketch1 = sketch(on = XY) {
14765}
14766";
14767
14768        let program = Program::parse(initial_source).unwrap().0.unwrap();
14769
14770        let mut frontend = FrontendState::new();
14771        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14772        let version = Version(0);
14773
14774        frontend.hack_set_program(&ctx, program).await.unwrap();
14775
14776        let sketch_args = SketchCtor {
14777            on: Plane::Default(PlaneName::Xy),
14778        };
14779        let (src_delta, _, _) = frontend
14780            .new_sketch(&ctx, ProjectId(0), FileId(0), version, sketch_args)
14781            .await
14782            .unwrap();
14783
14784        insta::assert_snapshot!("test_new_sketch_twice_using_same_plane", src_delta.text.as_str());
14785
14786        ctx.close().await;
14787    }
14788
14789    #[tokio::test(flavor = "multi_thread")]
14790    async fn test_sketch_mode_reuses_cached_on_expression() {
14791        let initial_source = "\
14792width = 2mm
14793sketch(on = offsetPlane(XY, offset = width)) {
14794  line1 = line(start = [var 0, var 0], end = [var 1mm, var 0])
14795  distance([line1.start, line1.end]) == width
14796}
14797";
14798        let program = Program::parse(initial_source).unwrap().0.unwrap();
14799
14800        let mut frontend = FrontendState::new();
14801        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14802        let mock_ctx = ExecutorContext::new_mock(None).await;
14803        let version = Version(0);
14804        let project_id = ProjectId(0);
14805        let file_id = FileId(0);
14806
14807        frontend.hack_set_program(&ctx, program).await.unwrap();
14808        let initial_object_count = frontend.scene_graph.objects.len();
14809        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14810            .expect("Expected sketch object to exist")
14811            .id;
14812
14813        // Entering sketch mode should reuse cached `on` expression state
14814        // (offsetPlane result), not fail or create extra on-surface objects.
14815        let scene_delta = frontend
14816            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
14817            .await
14818            .unwrap();
14819        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14820
14821        // A follow-up sketch-mode execution should keep the same stable object
14822        // graph shape as well.
14823        let (_src_delta, scene_delta) = frontend.execute_mock(&mock_ctx, version, sketch_id).await.unwrap();
14824        assert_eq!(scene_delta.new_graph.objects.len(), initial_object_count);
14825
14826        ctx.close().await;
14827        mock_ctx.close().await;
14828    }
14829
14830    #[tokio::test(flavor = "multi_thread")]
14831    async fn test_edit_sketch_nested_in_pipe() {
14832        clear_mem_cache().await;
14833        let source = r#"
14834profile = sketch(on = XY) {
14835  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
14836}
14837  |> translate(x = 2mm)
14838"#;
14839        let program = Program::parse_no_errs(source).unwrap();
14840        let mut frontend = FrontendState::new();
14841        let mock_ctx = ExecutorContext::new_mock(None).await;
14842        let version = Version(0);
14843
14844        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14845        let sketch_id = find_first_sketch_object(&frontend.scene_graph)
14846            .expect("Expected piped sketch object")
14847            .id;
14848
14849        let scene_delta = frontend
14850            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14851            .await
14852            .unwrap();
14853        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14854        assert!(
14855            scene_delta
14856                .new_graph
14857                .objects
14858                .iter()
14859                .any(|object| matches!(&object.kind, ObjectKind::Segment { .. })),
14860            "Expected the piped sketch's segments to be present in sketch mode"
14861        );
14862
14863        clear_mem_cache().await;
14864        mock_ctx.close().await;
14865    }
14866
14867    #[tokio::test(flavor = "multi_thread")]
14868    async fn test_issue_9409_edit_sketch_nested_in_if_with_var_feedback() {
14869        clear_mem_cache().await;
14870        let source = r#"
14871useFirstProfile = true
14872
14873profile = if useFirstProfile {
14874  sketch(on = XY) {
14875    line1 = line(start = [0mm, 0mm], end = [var 20mm, var 10mm])
14876  }
14877} else {
14878  sketch(on = XY) {
14879    line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])
14880  }
14881}
14882"#;
14883        let program = Program::parse_no_errs(source).unwrap();
14884        let mut frontend = FrontendState::new();
14885        let mock_ctx = ExecutorContext::new_mock(None).await;
14886        let version = Version(0);
14887
14888        seed_frontend_with_mock(&mut frontend, &mock_ctx, &program).await;
14889        let sketch_object =
14890            find_first_sketch_object(&frontend.scene_graph).expect("Expected active branch's sketch object");
14891        let sketch_id = sketch_object.id;
14892        let sketch = expect_sketch(sketch_object);
14893        let line_end_id = *sketch
14894            .segments
14895            .get(1)
14896            .expect("Expected the active branch's line end point");
14897
14898        let scene_delta = frontend
14899            .edit_sketch(&mock_ctx, ProjectId(0), FileId(0), version, sketch_id)
14900            .await
14901            .unwrap();
14902        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch_id));
14903
14904        let segments = vec![ExistingSegmentCtor {
14905            id: line_end_id,
14906            ctor: SegmentCtor::Point(PointCtor {
14907                position: Point2d {
14908                    x: Expr::Var(Number {
14909                        value: 30.0,
14910                        units: NumericSuffix::Mm,
14911                    }),
14912                    y: Expr::Var(Number {
14913                        value: 15.0,
14914                        units: NumericSuffix::Mm,
14915                    }),
14916                },
14917            }),
14918        }];
14919        let (source_delta, _) = frontend
14920            .edit_segments(&mock_ctx, version, sketch_id, segments)
14921            .await
14922            .unwrap();
14923        assert!(
14924            source_delta
14925                .text
14926                .contains("line1 = line(start = [0mm, 0mm], end = [var 30mm, var 15mm])"),
14927            "Expected the active branch's dragged variables to be updated:\n{}",
14928            source_delta.text
14929        );
14930        assert!(
14931            source_delta
14932                .text
14933                .contains("line2 = line(start = [0mm, 0mm], end = [var 10mm, var 20mm])"),
14934            "Expected the inactive branch to remain unchanged:\n{}",
14935            source_delta.text
14936        );
14937
14938        clear_mem_cache().await;
14939        mock_ctx.close().await;
14940    }
14941
14942    #[tokio::test(flavor = "multi_thread")]
14943    async fn test_multiple_sketch_blocks() {
14944        let initial_source = "\
14945// Cube that requires the engine.
14946width = 2
14947sketch001 = startSketchOn(XY)
14948profile001 = startProfile(sketch001, at = [0, 0])
14949  |> yLine(length = width, tag = $seg1)
14950  |> xLine(length = width)
14951  |> yLine(length = -width)
14952  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
14953  |> close()
14954extrude001 = extrude(profile001, length = width)
14955
14956// Get a value that requires the engine.
14957x = segLen(seg1)
14958
14959// Triangle with side length 2*x.
14960sketch(on = XY) {
14961  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14962  line2 = line(start = [var 1.283mm, var -0.781mm], end = [var -0.71mm, var -0.95mm])
14963  coincident([line1.end, line2.start])
14964  line3 = line(start = [var -0.71mm, var -0.95mm], end = [var 0.14mm, var 0.86mm])
14965  coincident([line2.end, line3.start])
14966  coincident([line3.end, line1.start])
14967  equalLength([line3, line1])
14968  equalLength([line1, line2])
14969  distance([line1.start, line1.end]) == 2*x
14970}
14971
14972// Line segment with length x.
14973sketch2 = sketch(on = XY) {
14974  line1 = line(start = [var 0.14mm, var 0.86mm], end = [var 1.283mm, var -0.781mm])
14975  distance([line1.start, line1.end]) == x
14976}
14977";
14978
14979        let program = Program::parse(initial_source).unwrap().0.unwrap();
14980
14981        let mut frontend = FrontendState::new();
14982
14983        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
14984        let mock_ctx = ExecutorContext::new_mock(None).await;
14985        let version = Version(0);
14986        let project_id = ProjectId(0);
14987        let file_id = FileId(0);
14988
14989        frontend.hack_set_program(&ctx, program).await.unwrap();
14990        let sketch_objects = frontend
14991            .scene_graph
14992            .objects
14993            .iter()
14994            .filter(|obj| matches!(obj.kind, ObjectKind::Sketch(_)))
14995            .collect::<Vec<_>>();
14996        let sketch1_id = sketch_objects.first().unwrap().id;
14997        let sketch2_id = sketch_objects.get(1).unwrap().id;
14998        // First point in sketch1.
14999        let point1_id = ObjectId(sketch1_id.0 + 1);
15000        // First point in sketch2.
15001        let point2_id = ObjectId(sketch2_id.0 + 1);
15002
15003        // Edit the first sketch. Objects before the sketch block should be
15004        // present from execution cache so that we can sketch on prior planes,
15005        // for example. Objects after the first sketch block should not be
15006        // present since those statements are skipped in sketch mode.
15007        //
15008        // - startSketchOn(XY) Plane 1
15009        // - sketch on=XY Plane 1
15010        // - Sketch block 16
15011        let scene_delta = frontend
15012            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15013            .await
15014            .unwrap();
15015        assert_eq!(
15016            scene_delta.new_graph.objects.len(),
15017            18,
15018            "{:#?}",
15019            scene_delta.new_graph.objects
15020        );
15021
15022        // Edit a point in the first sketch.
15023        let point_ctor = PointCtor {
15024            position: Point2d {
15025                x: Expr::Var(Number {
15026                    value: 1.0,
15027                    units: NumericSuffix::Mm,
15028                }),
15029                y: Expr::Var(Number {
15030                    value: 2.0,
15031                    units: NumericSuffix::Mm,
15032                }),
15033            },
15034        };
15035        let segments = vec![ExistingSegmentCtor {
15036            id: point1_id,
15037            ctor: SegmentCtor::Point(point_ctor),
15038        }];
15039        let (src_delta, _) = frontend
15040            .edit_segments(&mock_ctx, version, sketch1_id, segments)
15041            .await
15042            .unwrap();
15043        // Only the first sketch block changes.
15044        insta::assert_snapshot!("test_multiple_sketch_blocks_1", src_delta.text.as_str());
15045        let edited_sketch1_source = src_delta.text.clone();
15046
15047        // Execute mock to simulate drag end.
15048        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch1_id).await.unwrap();
15049        assert_eq!(src_delta.text, edited_sketch1_source);
15050        // Exit sketch. Objects from the entire program should be present.
15051        //
15052        // - startSketchOn(XY) Plane 1
15053        // - sketch on=XY Plane 1
15054        // - Sketch block 16
15055        // - sketch on=XY cached
15056        // - Sketch block 5
15057        let scene = frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15058        assert_eq!(scene.objects.len(), 30, "{:#?}", scene.objects);
15059
15060        // Edit the second sketch.
15061        //
15062        // - startSketchOn(XY) Plane 1
15063        // - sketch on=XY Plane 1
15064        // - Sketch block 16
15065        // - sketch on=XY cached
15066        // - Sketch block 5
15067        let scene_delta = frontend
15068            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15069            .await
15070            .unwrap();
15071        assert_eq!(
15072            scene_delta.new_graph.objects.len(),
15073            24,
15074            "{:#?}",
15075            scene_delta.new_graph.objects
15076        );
15077
15078        // Edit a point in the second sketch.
15079        let point_ctor = PointCtor {
15080            position: Point2d {
15081                x: Expr::Var(Number {
15082                    value: 3.0,
15083                    units: NumericSuffix::Mm,
15084                }),
15085                y: Expr::Var(Number {
15086                    value: 4.0,
15087                    units: NumericSuffix::Mm,
15088                }),
15089            },
15090        };
15091        let segments = vec![ExistingSegmentCtor {
15092            id: point2_id,
15093            ctor: SegmentCtor::Point(point_ctor),
15094        }];
15095        let (src_delta, _) = frontend
15096            .edit_segments(&mock_ctx, version, sketch2_id, segments)
15097            .await
15098            .unwrap();
15099        // Only the second sketch block changes.
15100        insta::assert_snapshot!("test_multiple_sketch_blocks_2", src_delta.text.as_str());
15101        let edited_sketch2_source = src_delta.text.clone();
15102
15103        // Execute mock to simulate drag end.
15104        let (src_delta, _) = frontend.execute_mock(&mock_ctx, version, sketch2_id).await.unwrap();
15105        assert_eq!(src_delta.text, edited_sketch2_source);
15106
15107        ctx.close().await;
15108        mock_ctx.close().await;
15109    }
15110
15111    #[tokio::test(flavor = "multi_thread")]
15112    async fn test_exit_sketch_without_changes_allows_entering_next_sketch() {
15113        clear_mem_cache().await;
15114
15115        let source = r#"sketch001 = sketch(on = XZ) {
15116  circle1 = circle(start = [var -1.96mm, var 2.77mm], center = [var -2.69mm, var 3.44mm])
15117}
15118sketch002 = sketch(on = XY) {
15119  line1 = line(start = [var 0mm, var 0mm], end = [var 4.68mm, var 0mm])
15120  line2 = line(start = [var 4.68mm, var 0mm], end = [var 4.68mm, var 2.96mm])
15121  line3 = line(start = [var 4.68mm, var 2.96mm], end = [var 0mm, var 2.96mm])
15122  line4 = line(start = [var 0mm, var 2.96mm], end = [var 0mm, var 0mm])
15123  coincident([line1.end, line2.start])
15124  coincident([line2.end, line3.start])
15125  coincident([line3.end, line4.start])
15126  coincident([line4.end, line1.start])
15127  parallel([line2, line4])
15128  parallel([line3, line1])
15129  perpendicular([line1, line2])
15130  horizontal(line3)
15131  coincident([line1.start, ORIGIN])
15132}
15133"#;
15134
15135        let program = Program::parse(source).unwrap().0.unwrap();
15136        let mut frontend = FrontendState::new();
15137        let ctx = ExecutorContext::new_with_engine(sync::Arc::new(EngineManager::new_mock()), Default::default());
15138        let mock_ctx = ExecutorContext::new_mock(None).await;
15139        let version = Version(0);
15140        let project_id = ProjectId(0);
15141        let file_id = FileId(0);
15142
15143        frontend.hack_set_program(&ctx, program).await.unwrap();
15144        let sketch_objects = frontend
15145            .scene_graph
15146            .objects
15147            .iter()
15148            .filter(|object| matches!(object.kind, ObjectKind::Sketch(_)))
15149            .collect::<Vec<_>>();
15150        assert_eq!(sketch_objects.len(), 2, "{:#?}", frontend.scene_graph.objects);
15151
15152        let sketch1_id = sketch_objects[0].id;
15153        let sketch2_id = sketch_objects[1].id;
15154
15155        frontend
15156            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch1_id)
15157            .await
15158            .unwrap();
15159        frontend.exit_sketch(&ctx, version, sketch1_id).await.unwrap();
15160
15161        let scene_delta = frontend
15162            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch2_id)
15163            .await
15164            .unwrap();
15165        assert_eq!(scene_delta.new_graph.sketch_mode, Some(sketch2_id));
15166
15167        clear_mem_cache().await;
15168        ctx.close().await;
15169        mock_ctx.close().await;
15170    }
15171
15172    // Regression tests: operations on source code with extra whitespace/newlines.
15173    // These test that NodePath-based lookups work correctly when source ranges
15174    // are shifted by extra whitespace that wouldn't be present after formatting.
15175
15176    #[tokio::test(flavor = "multi_thread")]
15177    async fn test_extra_newlines_after_settings_edit_sketch_add_point() {
15178        // Extra newlines after @settings line - this shifts all source ranges.
15179        let initial_source = "@settings(defaultLengthUnit = mm)
15180
15181sketch001 = sketch(on = XY) {
15182  point(at = [1in, 2in])
15183}
15184";
15185
15186        let program = Program::parse(initial_source).unwrap().0.unwrap();
15187        let mut frontend = FrontendState::new();
15188
15189        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15190        let mock_ctx = ExecutorContext::new_mock(None).await;
15191        let version = Version(0);
15192        let project_id = ProjectId(0);
15193        let file_id = FileId(0);
15194
15195        frontend.hack_set_program(&ctx, program).await.unwrap();
15196        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15197        let sketch_id = sketch_object.id;
15198
15199        // Edit sketch should succeed despite extra newlines.
15200        frontend
15201            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15202            .await
15203            .unwrap();
15204
15205        // Add a new point to the sketch.
15206        let point_ctor = PointCtor {
15207            position: Point2d {
15208                x: Expr::Number(Number {
15209                    value: 5.0,
15210                    units: NumericSuffix::Mm,
15211                }),
15212                y: Expr::Number(Number {
15213                    value: 6.0,
15214                    units: NumericSuffix::Mm,
15215                }),
15216            },
15217        };
15218        let segment = SegmentCtor::Point(point_ctor);
15219        let (src_delta, scene_delta) = frontend
15220            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15221            .await
15222            .unwrap();
15223        // After adding a point, the source should be reformatted with standard whitespace.
15224        assert!(
15225            src_delta.text.contains("point(at = [5mm, 6mm])"),
15226            "Expected new point in source, got: {}",
15227            src_delta.text
15228        );
15229        assert!(!scene_delta.new_objects.is_empty());
15230
15231        ctx.close().await;
15232        mock_ctx.close().await;
15233    }
15234
15235    #[tokio::test(flavor = "multi_thread")]
15236    async fn test_ensure_control_point_spline_experimental_features_adds_allow_setting() {
15237        let initial_program = Program::parse("s = sketch(on = XY) {}\n").unwrap().0.unwrap();
15238
15239        let updated_program = ensure_control_point_spline_experimental_features(&initial_program).unwrap();
15240        let meta_settings = updated_program.meta_settings().unwrap().unwrap();
15241
15242        assert_eq!(meta_settings.experimental_features, WarningLevel::Allow);
15243        assert!(
15244            source_from_ast(&updated_program.ast).contains("@settings(experimentalFeatures = allow)"),
15245            "Expected experimental settings to be added to source"
15246        );
15247    }
15248
15249    #[tokio::test(flavor = "multi_thread")]
15250    async fn test_extra_newlines_after_settings_add_line_to_empty_sketch() {
15251        // Extra newlines after @settings, with an empty sketch block.
15252        let initial_source = "@settings(defaultLengthUnit = mm)
15253
15254s = sketch(on = XY) {}
15255";
15256
15257        let program = Program::parse(initial_source).unwrap().0.unwrap();
15258        let mut frontend = FrontendState::new();
15259
15260        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15261        let mock_ctx = ExecutorContext::new_mock(None).await;
15262        let version = Version(0);
15263
15264        frontend.hack_set_program(&ctx, program).await.unwrap();
15265        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15266        let sketch_id = sketch_object.id;
15267
15268        let line_ctor = LineCtor {
15269            start: Point2d {
15270                x: Expr::Number(Number {
15271                    value: 0.0,
15272                    units: NumericSuffix::Mm,
15273                }),
15274                y: Expr::Number(Number {
15275                    value: 0.0,
15276                    units: NumericSuffix::Mm,
15277                }),
15278            },
15279            end: Point2d {
15280                x: Expr::Number(Number {
15281                    value: 10.0,
15282                    units: NumericSuffix::Mm,
15283                }),
15284                y: Expr::Number(Number {
15285                    value: 10.0,
15286                    units: NumericSuffix::Mm,
15287                }),
15288            },
15289            construction: None,
15290        };
15291        let segment = SegmentCtor::Line(line_ctor);
15292        let (src_delta, scene_delta) = frontend
15293            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15294            .await
15295            .unwrap();
15296        assert!(
15297            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15298            "Expected line in source, got: {}",
15299            src_delta.text
15300        );
15301        // Line creates start point, end point, and line segment.
15302        assert_eq!(scene_delta.new_objects.len(), 3);
15303
15304        ctx.close().await;
15305        mock_ctx.close().await;
15306    }
15307
15308    #[tokio::test(flavor = "multi_thread")]
15309    async fn test_extra_newlines_between_operations_edit_line() {
15310        // Extra newlines between @settings and sketch, and inside the sketch block.
15311        let initial_source = "@settings(defaultLengthUnit = mm)
15312
15313sketch001 = sketch(on = XY) {
15314
15315  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15316
15317}
15318";
15319
15320        let program = Program::parse(initial_source).unwrap().0.unwrap();
15321        let mut frontend = FrontendState::new();
15322
15323        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15324        let mock_ctx = ExecutorContext::new_mock(None).await;
15325        let version = Version(0);
15326        let project_id = ProjectId(0);
15327        let file_id = FileId(0);
15328
15329        let outcome = frontend.hack_set_program(&ctx, program).await.unwrap();
15330        assert!(matches!(outcome, SetProgramOutcome::Success { .. }), "{outcome:?}");
15331        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15332        let sketch_id = sketch_object.id;
15333        let sketch = expect_sketch(sketch_object);
15334
15335        // Extract segment IDs before edit_sketch borrows frontend mutably.
15336        let line_id = sketch
15337            .segments
15338            .iter()
15339            .copied()
15340            .find(|seg_id| {
15341                matches!(
15342                    &frontend.scene_graph.objects[seg_id.0].kind,
15343                    ObjectKind::Segment {
15344                        segment: Segment::Line(_)
15345                    }
15346                )
15347            })
15348            .expect("Expected a line segment in sketch");
15349
15350        // Enter sketch edit mode.
15351        frontend
15352            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15353            .await
15354            .unwrap();
15355
15356        // Edit the line.
15357        let line_ctor = LineCtor {
15358            start: Point2d {
15359                x: Expr::Var(Number {
15360                    value: 1.0,
15361                    units: NumericSuffix::Mm,
15362                }),
15363                y: Expr::Var(Number {
15364                    value: 2.0,
15365                    units: NumericSuffix::Mm,
15366                }),
15367            },
15368            end: Point2d {
15369                x: Expr::Var(Number {
15370                    value: 13.0,
15371                    units: NumericSuffix::Mm,
15372                }),
15373                y: Expr::Var(Number {
15374                    value: 14.0,
15375                    units: NumericSuffix::Mm,
15376                }),
15377            },
15378            construction: None,
15379        };
15380        let segments = vec![ExistingSegmentCtor {
15381            id: line_id,
15382            ctor: SegmentCtor::Line(line_ctor),
15383        }];
15384        let (src_delta, _scene_delta) = frontend
15385            .edit_segments(&mock_ctx, version, sketch_id, segments)
15386            .await
15387            .unwrap();
15388        assert!(
15389            src_delta
15390                .text
15391                .contains("line(start = [var 1mm, var 2mm], end = [var 13mm, var 14mm])"),
15392            "Expected edited line in source, got: {}",
15393            src_delta.text
15394        );
15395
15396        ctx.close().await;
15397        mock_ctx.close().await;
15398    }
15399
15400    #[tokio::test(flavor = "multi_thread")]
15401    async fn test_extra_newlines_delete_segment() {
15402        // Extra whitespace before and after the sketch block.
15403        let initial_source = "@settings(defaultLengthUnit = mm)
15404
15405sketch001 = sketch(on = XY) {
15406  circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
15407}
15408";
15409
15410        let program = Program::parse(initial_source).unwrap().0.unwrap();
15411        let mut frontend = FrontendState::new();
15412
15413        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15414        let mock_ctx = ExecutorContext::new_mock(None).await;
15415        let version = Version(0);
15416
15417        frontend.hack_set_program(&ctx, program).await.unwrap();
15418        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15419        let sketch_id = sketch_object.id;
15420        let sketch = expect_sketch(sketch_object);
15421
15422        // The sketch should have 3 segments: start point, center point, and the circle.
15423        assert_eq!(sketch.segments.len(), 3);
15424        let circle_id = sketch.segments[2];
15425
15426        // Delete the circle despite extra newlines in original source.
15427        let (src_delta, scene_delta) = frontend
15428            .delete_objects(&mock_ctx, version, sketch_id, vec![], vec![circle_id])
15429            .await
15430            .unwrap();
15431        assert!(
15432            src_delta.text.contains("sketch(on = XY) {"),
15433            "Expected sketch block in source, got: {}",
15434            src_delta.text
15435        );
15436        let new_sketch_object = find_first_sketch_object(&scene_delta.new_graph).unwrap();
15437        let new_sketch = expect_sketch(new_sketch_object);
15438        assert_eq!(new_sketch.segments.len(), 0);
15439
15440        ctx.close().await;
15441        mock_ctx.close().await;
15442    }
15443
15444    #[tokio::test(flavor = "multi_thread")]
15445    async fn test_unformatted_source_add_arc() {
15446        // Source with inconsistent whitespace - tabs, extra spaces, multiple blank lines.
15447        let initial_source = "@settings(defaultLengthUnit = mm)
15448
15449sketch001 = sketch(on = XY) {
15450}
15451";
15452
15453        let program = Program::parse(initial_source).unwrap().0.unwrap();
15454        let mut frontend = FrontendState::new();
15455
15456        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15457        let mock_ctx = ExecutorContext::new_mock(None).await;
15458        let version = Version(0);
15459
15460        frontend.hack_set_program(&ctx, program).await.unwrap();
15461        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15462        let sketch_id = sketch_object.id;
15463
15464        let arc_ctor = ArcCtor {
15465            start: Point2d {
15466                x: Expr::Var(Number {
15467                    value: 5.0,
15468                    units: NumericSuffix::Mm,
15469                }),
15470                y: Expr::Var(Number {
15471                    value: 0.0,
15472                    units: NumericSuffix::Mm,
15473                }),
15474            },
15475            end: Point2d {
15476                x: Expr::Var(Number {
15477                    value: 0.0,
15478                    units: NumericSuffix::Mm,
15479                }),
15480                y: Expr::Var(Number {
15481                    value: 5.0,
15482                    units: NumericSuffix::Mm,
15483                }),
15484            },
15485            center: Point2d {
15486                x: Expr::Var(Number {
15487                    value: 0.0,
15488                    units: NumericSuffix::Mm,
15489                }),
15490                y: Expr::Var(Number {
15491                    value: 0.0,
15492                    units: NumericSuffix::Mm,
15493                }),
15494            },
15495            direction: None,
15496            construction: None,
15497        };
15498        let segment = SegmentCtor::Arc(arc_ctor);
15499        let (src_delta, scene_delta) = frontend
15500            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15501            .await
15502            .unwrap();
15503        assert!(
15504            src_delta
15505                .text
15506                .contains("arc(start = [var 5mm, var 0mm], end = [var 0mm, var 5mm], center = [var 0mm, var 0mm])"),
15507            "Expected arc in source, got: {}",
15508            src_delta.text
15509        );
15510        assert!(!scene_delta.new_objects.is_empty());
15511
15512        ctx.close().await;
15513        mock_ctx.close().await;
15514    }
15515
15516    #[tokio::test(flavor = "multi_thread")]
15517    async fn test_arc_direction_flows_to_source() {
15518        let initial_source = "@settings(defaultLengthUnit = mm)
15519
15520sketch001 = sketch(on = XY) {
15521}
15522";
15523
15524        let program = Program::parse(initial_source).unwrap().0.unwrap();
15525        let mut frontend = FrontendState::new();
15526
15527        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15528        let mock_ctx = ExecutorContext::new_mock(None).await;
15529        let version = Version(0);
15530
15531        frontend.hack_set_program(&ctx, program).await.unwrap();
15532        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15533        let sketch_id = sketch_object.id;
15534
15535        let point = |x: f64, y: f64| Point2d {
15536            x: Expr::Var(Number {
15537                value: x,
15538                units: NumericSuffix::Mm,
15539            }),
15540            y: Expr::Var(Number {
15541                value: y,
15542                units: NumericSuffix::Mm,
15543            }),
15544        };
15545
15546        // Adding a clockwise arc writes its direction to the source.
15547        let arc_ctor = ArcCtor {
15548            start: point(5.0, 0.0),
15549            end: point(0.0, 5.0),
15550            center: point(0.0, 0.0),
15551            direction: Some(ArcDirection::Cw),
15552            construction: None,
15553        };
15554        let (src_delta, scene_delta) = frontend
15555            .add_segment(&mock_ctx, version, sketch_id, SegmentCtor::Arc(arc_ctor), None)
15556            .await
15557            .unwrap();
15558        assert!(
15559            src_delta.text.contains("direction = CW"),
15560            "Expected direction = CW in source, got: {}",
15561            src_delta.text
15562        );
15563        // The new objects are the end points, the center, and then the arc.
15564        let arc_id = *scene_delta.new_objects.last().unwrap();
15565
15566        // Editing the arc's points preserves the clockwise direction. The
15567        // edited points keep the same distance to the center so that the
15568        // solver doesn't need to move anything.
15569        let edited_ctor = ArcCtor {
15570            start: point(0.0, -5.0),
15571            end: point(0.0, 5.0),
15572            center: point(0.0, 0.0),
15573            direction: Some(ArcDirection::Cw),
15574            construction: None,
15575        };
15576        let (src_delta, _scene_delta) = frontend
15577            .edit_segments(
15578                &mock_ctx,
15579                version,
15580                sketch_id,
15581                vec![ExistingSegmentCtor {
15582                    id: arc_id,
15583                    ctor: SegmentCtor::Arc(edited_ctor),
15584                }],
15585            )
15586            .await
15587            .unwrap();
15588        assert!(
15589            src_delta.text.contains("start = [var 0mm, var -5mm]"),
15590            "Expected edited start point in source, got: {}",
15591            src_delta.text
15592        );
15593        assert!(
15594            src_delta.text.contains("direction = CW"),
15595            "Expected direction = CW to be preserved in source, got: {}",
15596            src_delta.text
15597        );
15598
15599        // Editing the arc back to counterclockwise removes the direction
15600        // argument since counterclockwise is the default.
15601        let edited_ctor = ArcCtor {
15602            start: point(0.0, -5.0),
15603            end: point(0.0, 5.0),
15604            center: point(0.0, 0.0),
15605            direction: Some(ArcDirection::Ccw),
15606            construction: None,
15607        };
15608        let (src_delta, _scene_delta) = frontend
15609            .edit_segments(
15610                &mock_ctx,
15611                version,
15612                sketch_id,
15613                vec![ExistingSegmentCtor {
15614                    id: arc_id,
15615                    ctor: SegmentCtor::Arc(edited_ctor),
15616                }],
15617            )
15618            .await
15619            .unwrap();
15620        assert!(
15621            !src_delta.text.contains("direction"),
15622            "Expected direction argument to be removed from source, got: {}",
15623            src_delta.text
15624        );
15625
15626        ctx.close().await;
15627        mock_ctx.close().await;
15628    }
15629
15630    #[tokio::test(flavor = "multi_thread")]
15631    async fn test_extra_newlines_add_circle() {
15632        // Extra blank lines between settings and sketch.
15633        let initial_source = "@settings(defaultLengthUnit = mm)
15634
15635sketch001 = sketch(on = XY) {
15636}
15637";
15638
15639        let program = Program::parse(initial_source).unwrap().0.unwrap();
15640        let mut frontend = FrontendState::new();
15641
15642        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15643        let mock_ctx = ExecutorContext::new_mock(None).await;
15644        let version = Version(0);
15645
15646        frontend.hack_set_program(&ctx, program).await.unwrap();
15647        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15648        let sketch_id = sketch_object.id;
15649
15650        let circle_ctor = CircleCtor {
15651            start: Point2d {
15652                x: Expr::Var(Number {
15653                    value: 5.0,
15654                    units: NumericSuffix::Mm,
15655                }),
15656                y: Expr::Var(Number {
15657                    value: 0.0,
15658                    units: NumericSuffix::Mm,
15659                }),
15660            },
15661            center: Point2d {
15662                x: Expr::Var(Number {
15663                    value: 0.0,
15664                    units: NumericSuffix::Mm,
15665                }),
15666                y: Expr::Var(Number {
15667                    value: 0.0,
15668                    units: NumericSuffix::Mm,
15669                }),
15670            },
15671            construction: None,
15672        };
15673        let segment = SegmentCtor::Circle(circle_ctor);
15674        let (src_delta, scene_delta) = frontend
15675            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15676            .await
15677            .unwrap();
15678        assert!(
15679            src_delta
15680                .text
15681                .contains("circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])"),
15682            "Expected circle in source, got: {}",
15683            src_delta.text
15684        );
15685        assert!(!scene_delta.new_objects.is_empty());
15686
15687        ctx.close().await;
15688        mock_ctx.close().await;
15689    }
15690
15691    #[tokio::test(flavor = "multi_thread")]
15692    async fn test_extra_newlines_add_constraint() {
15693        // Extra newlines with a sketch containing two lines - add a coincident constraint.
15694        let initial_source = "@settings(defaultLengthUnit = mm)
15695
15696sketch001 = sketch(on = XY) {
15697  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 10mm])
15698  line2 = line(start = [var 10mm, var 10mm], end = [var 20mm, var 0mm])
15699}
15700";
15701
15702        let program = Program::parse(initial_source).unwrap().0.unwrap();
15703        let mut frontend = FrontendState::new();
15704
15705        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15706        let mock_ctx = ExecutorContext::new_mock(None).await;
15707        let version = Version(0);
15708        let project_id = ProjectId(0);
15709        let file_id = FileId(0);
15710
15711        frontend.hack_set_program(&ctx, program).await.unwrap();
15712        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15713        let sketch_id = sketch_object.id;
15714        let sketch = expect_sketch(sketch_object);
15715
15716        // Extract segment data before edit_sketch borrows frontend mutably.
15717        let line_ids: Vec<ObjectId> = sketch
15718            .segments
15719            .iter()
15720            .copied()
15721            .filter(|seg_id| {
15722                matches!(
15723                    &frontend.scene_graph.objects[seg_id.0].kind,
15724                    ObjectKind::Segment {
15725                        segment: Segment::Line(_)
15726                    }
15727                )
15728            })
15729            .collect();
15730        assert_eq!(line_ids.len(), 2, "Expected two line segments");
15731
15732        let line1 = &frontend.scene_graph.objects[line_ids[0].0];
15733        let ObjectKind::Segment {
15734            segment: Segment::Line(line1_data),
15735        } = &line1.kind
15736        else {
15737            panic!("Expected line");
15738        };
15739        let line2 = &frontend.scene_graph.objects[line_ids[1].0];
15740        let ObjectKind::Segment {
15741            segment: Segment::Line(line2_data),
15742        } = &line2.kind
15743        else {
15744            panic!("Expected line");
15745        };
15746
15747        // Build constraint before entering sketch mode.
15748        let constraint = Constraint::Coincident(Coincident {
15749            segments: vec![line1_data.end.into(), line2_data.start.into()],
15750        });
15751
15752        // Enter sketch edit mode.
15753        frontend
15754            .edit_sketch(&mock_ctx, project_id, file_id, version, sketch_id)
15755            .await
15756            .unwrap();
15757        let (src_delta, _scene_delta) = frontend
15758            .add_constraint(&mock_ctx, version, sketch_id, constraint)
15759            .await
15760            .unwrap();
15761        assert!(
15762            src_delta.text.contains("coincident("),
15763            "Expected coincident constraint in source, got: {}",
15764            src_delta.text
15765        );
15766
15767        ctx.close().await;
15768        mock_ctx.close().await;
15769    }
15770
15771    #[tokio::test(flavor = "multi_thread")]
15772    async fn test_extra_newlines_add_line_then_edit_line() {
15773        // Extra newlines after @settings - add a line, then edit it.
15774        let initial_source = "@settings(defaultLengthUnit = mm)
15775
15776sketch001 = sketch(on = XY) {
15777}
15778";
15779
15780        let program = Program::parse(initial_source).unwrap().0.unwrap();
15781        let mut frontend = FrontendState::new();
15782
15783        let ctx = ExecutorContext::new_geometry_only_with_default_client().await.unwrap();
15784        let mock_ctx = ExecutorContext::new_mock(None).await;
15785        let version = Version(0);
15786
15787        frontend.hack_set_program(&ctx, program).await.unwrap();
15788        let sketch_object = find_first_sketch_object(&frontend.scene_graph).unwrap();
15789        let sketch_id = sketch_object.id;
15790
15791        // Add a line.
15792        let line_ctor = LineCtor {
15793            start: Point2d {
15794                x: Expr::Number(Number {
15795                    value: 0.0,
15796                    units: NumericSuffix::Mm,
15797                }),
15798                y: Expr::Number(Number {
15799                    value: 0.0,
15800                    units: NumericSuffix::Mm,
15801                }),
15802            },
15803            end: Point2d {
15804                x: Expr::Number(Number {
15805                    value: 10.0,
15806                    units: NumericSuffix::Mm,
15807                }),
15808                y: Expr::Number(Number {
15809                    value: 10.0,
15810                    units: NumericSuffix::Mm,
15811                }),
15812            },
15813            construction: None,
15814        };
15815        let segment = SegmentCtor::Line(line_ctor);
15816        let (src_delta, scene_delta) = frontend
15817            .add_segment(&mock_ctx, version, sketch_id, segment, None)
15818            .await
15819            .unwrap();
15820        assert!(
15821            src_delta.text.contains("line(start = [0mm, 0mm], end = [10mm, 10mm])"),
15822            "Expected line in source after add, got: {}",
15823            src_delta.text
15824        );
15825        // Line creates start point, end point, and line segment.
15826        let line_id = *scene_delta.new_objects.last().unwrap();
15827
15828        // Edit the line.
15829        let line_ctor = LineCtor {
15830            start: Point2d {
15831                x: Expr::Number(Number {
15832                    value: 1.0,
15833                    units: NumericSuffix::Mm,
15834                }),
15835                y: Expr::Number(Number {
15836                    value: 2.0,
15837                    units: NumericSuffix::Mm,
15838                }),
15839            },
15840            end: Point2d {
15841                x: Expr::Number(Number {
15842                    value: 13.0,
15843                    units: NumericSuffix::Mm,
15844                }),
15845                y: Expr::Number(Number {
15846                    value: 14.0,
15847                    units: NumericSuffix::Mm,
15848                }),
15849            },
15850            construction: None,
15851        };
15852        let segments = vec![ExistingSegmentCtor {
15853            id: line_id,
15854            ctor: SegmentCtor::Line(line_ctor),
15855        }];
15856        let (src_delta, scene_delta) = frontend
15857            .edit_segments(&mock_ctx, version, sketch_id, segments)
15858            .await
15859            .unwrap();
15860        assert!(
15861            src_delta.text.contains("line(start = [1mm, 2mm], end = [13mm, 14mm])"),
15862            "Expected edited line in source, got: {}",
15863            src_delta.text
15864        );
15865        assert_eq!(scene_delta.new_objects, vec![]);
15866
15867        ctx.close().await;
15868        mock_ctx.close().await;
15869    }
15870
15871    #[test]
15872    fn test_add_variable_declaration_uses_top_level_scope_after_sketch_block() {
15873        // A non-target sketch block appears before the target so that the
15874        // traversal enters and leaves it before reaching the target. The
15875        // generated name must come from the top-level scope, where foo1 is
15876        // taken, not the sketch's scope, where no foo names are taken. This
15877        // is a regression test: dfs_mut used to visit the sketch block twice,
15878        // pushing its scope twice but popping it once, leaving the sketch
15879        // scope on top of the defined-names stack for the rest of the
15880        // traversal.
15881        let code = "\
15882foo1 = 1
15883sk = sketch() {
15884  p = var 1.5
15885}
158867 + 8
15887";
15888        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15889        let ast::BodyItem::ExpressionStatement(stmt) = &ast.body[2] else {
15890            panic!("expected an expression statement");
15891        };
15892        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15893        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15894            &mut ast,
15895            &source_ref,
15896            AstMutateCommand::AddVariableDeclaration {
15897                prefix: "foo".to_owned(),
15898            },
15899        )
15900        .unwrap();
15901        let AstMutateCommandReturn::Name(name) = cmd_return else {
15902            panic!("expected a generated name");
15903        };
15904        assert_eq!(name, "foo2");
15905        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[2] else {
15906            panic!("expected the expression statement to become a variable declaration");
15907        };
15908        assert_eq!(decl.name(), "foo2");
15909    }
15910
15911    /// Get the function body of the variable declaration at `ast.body[index]`.
15912    fn function_body_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15913        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15914            panic!("expected a variable declaration");
15915        };
15916        let ast::Expr::FunctionExpression(func) = &decl.declaration.init else {
15917            panic!("expected a function expression");
15918        };
15919        &func.body
15920    }
15921
15922    /// Get the then-branch block of the if-expression initializing the
15923    /// variable declaration at `ast.body[index]`.
15924    fn then_block_at(ast: &ast::Node<ast::Program>, index: usize) -> &ast::Node<ast::Program> {
15925        let ast::BodyItem::VariableDeclaration(decl) = &ast.body[index] else {
15926            panic!("expected a variable declaration");
15927        };
15928        let ast::Expr::IfExpression(if_expr) = &decl.declaration.init else {
15929            panic!("expected an if expression");
15930        };
15931        &if_expr.then_val
15932    }
15933
15934    #[test]
15935    fn test_add_variable_declaration_in_function_body_uses_function_scope() {
15936        // The generated name must come from the function body's scope, where
15937        // thing1 is taken. Before dfs_mut visited function bodies as program
15938        // nodes, the top-level scope was used instead, generating thing1 and
15939        // colliding with the local.
15940        let code = "\
15941fn build() {
15942  thing1 = 1
15943  10 + 20
15944  return thing1
15945}
15946";
15947        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15948        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
15949            panic!("expected an expression statement");
15950        };
15951        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
15952        let (_, cmd_return) = mutate_ast_node_by_source_ref(
15953            &mut ast,
15954            &source_ref,
15955            AstMutateCommand::AddVariableDeclaration {
15956                prefix: "thing".to_owned(),
15957            },
15958        )
15959        .unwrap();
15960        let AstMutateCommandReturn::Name(name) = cmd_return else {
15961            panic!("expected a generated name");
15962        };
15963        assert_eq!(name, "thing2");
15964        let body = &function_body_at(&ast, 0).body;
15965        assert_eq!(body.len(), 3);
15966        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
15967            panic!("expected the expression statement to become a variable declaration");
15968        };
15969        assert_eq!(decl.name(), "thing2");
15970        // Siblings are untouched.
15971        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
15972            panic!("expected a variable declaration");
15973        };
15974        assert_eq!(first.name(), "thing1");
15975        assert!(matches!(&body[2], ast::BodyItem::ReturnStatement(_)));
15976    }
15977
15978    #[test]
15979    fn test_delete_node_in_function_body_preserves_leading_comment() {
15980        // Before the shared body traversal, MutateBodyItem::Delete was
15981        // silently dropped inside function bodies, so this reported success
15982        // without deleting anything.
15983        let code = "\
15984fn build() {
15985  a = 1
15986  // keep me
15987  b = 2
15988  return a
15989}
15990";
15991        let mut ast = crate::parsing::top_level_parse(code).unwrap();
15992        let ast::BodyItem::VariableDeclaration(b_decl) = &function_body_at(&ast, 0).body[1] else {
15993            panic!("expected a variable declaration");
15994        };
15995        assert_eq!(b_decl.name(), "b");
15996        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
15997        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
15998        let body = &function_body_at(&ast, 0).body;
15999        assert_eq!(body.len(), 2, "expected b to be deleted");
16000        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16001            panic!("expected a variable declaration");
16002        };
16003        assert_eq!(first.name(), "a");
16004        let ast::BodyItem::ReturnStatement(_) = &body[1] else {
16005            panic!("expected the return statement to remain");
16006        };
16007        assert!(
16008            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16009            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16010            body[1].get_comments()
16011        );
16012    }
16013
16014    #[test]
16015    fn test_add_variable_declaration_in_function_body_ignores_parameters() {
16016        // Locals in the function body are avoided: thing1 is taken, so the
16017        // generated name is thing2. But find_defined_names only sees the
16018        // block's body items, not the function's parameters, so the generated
16019        // name collides with the thing2 parameter. This pins the current
16020        // behavior.
16021        // TODO: Should function parameters be included in the scope used for
16022        // name generation?
16023        let code = "\
16024fn build(thing2) {
16025  thing1 = 1
16026  10 + 20
16027  return thing1 + thing2
16028}
16029";
16030        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16031        let ast::BodyItem::ExpressionStatement(stmt) = &function_body_at(&ast, 0).body[1] else {
16032            panic!("expected an expression statement");
16033        };
16034        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16035        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16036            &mut ast,
16037            &source_ref,
16038            AstMutateCommand::AddVariableDeclaration {
16039                prefix: "thing".to_owned(),
16040            },
16041        )
16042        .unwrap();
16043        let AstMutateCommandReturn::Name(name) = cmd_return else {
16044            panic!("expected a generated name");
16045        };
16046        assert_eq!(name, "thing2", "locals are avoided, but parameters are not");
16047    }
16048
16049    #[test]
16050    fn test_add_variable_declaration_in_if_branch_uses_branch_scope() {
16051        let code = "\
16052x = 1
16053y = if x > 0 {
16054  q1 = 1
16055  foo(q1)
16056  q1
16057} else {
16058  2
16059}
16060";
16061        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16062        let ast::BodyItem::ExpressionStatement(stmt) = &then_block_at(&ast, 1).body[1] else {
16063            panic!("expected an expression statement");
16064        };
16065        let source_ref = SourceRef::new(SourceRange::from(&stmt.expression), None);
16066        let (_, cmd_return) = mutate_ast_node_by_source_ref(
16067            &mut ast,
16068            &source_ref,
16069            AstMutateCommand::AddVariableDeclaration { prefix: "q".to_owned() },
16070        )
16071        .unwrap();
16072        let AstMutateCommandReturn::Name(name) = cmd_return else {
16073            panic!("expected a generated name");
16074        };
16075        assert_eq!(name, "q2");
16076        let body = &then_block_at(&ast, 1).body;
16077        assert_eq!(body.len(), 3);
16078        let ast::BodyItem::VariableDeclaration(decl) = &body[1] else {
16079            panic!("expected the expression statement to become a variable declaration");
16080        };
16081        assert_eq!(decl.name(), "q2");
16082        // Siblings are untouched.
16083        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16084            panic!("expected a variable declaration");
16085        };
16086        assert_eq!(first.name(), "q1");
16087        assert!(matches!(&body[2], ast::BodyItem::ExpressionStatement(_)));
16088    }
16089
16090    #[test]
16091    fn test_delete_node_in_if_branch_preserves_leading_comment() {
16092        // Before the shared body traversal, MutateBodyItem::Delete was
16093        // silently dropped inside if-expression branch blocks.
16094        let code = "\
16095y = if true {
16096  a = 1
16097  // keep me
16098  b = 2
16099  a + b
16100} else {
16101  2
16102}
16103";
16104        let mut ast = crate::parsing::top_level_parse(code).unwrap();
16105        let ast::BodyItem::VariableDeclaration(b_decl) = &then_block_at(&ast, 0).body[1] else {
16106            panic!("expected a variable declaration");
16107        };
16108        assert_eq!(b_decl.name(), "b");
16109        let source_ref = SourceRef::new(SourceRange::from(&b_decl.declaration.init), None);
16110        mutate_ast_node_by_source_ref(&mut ast, &source_ref, AstMutateCommand::DeleteNode).unwrap();
16111        let body = &then_block_at(&ast, 0).body;
16112        assert_eq!(body.len(), 2, "expected b to be deleted");
16113        let ast::BodyItem::VariableDeclaration(first) = &body[0] else {
16114            panic!("expected a variable declaration");
16115        };
16116        assert_eq!(first.name(), "a");
16117        let ast::BodyItem::ExpressionStatement(_) = &body[1] else {
16118            panic!("expected the tail expression to remain");
16119        };
16120        assert!(
16121            body[1].get_comments().iter().any(|c| c.contains("keep me")),
16122            "expected the deleted item's leading comment to migrate to the next item, got: {:?}",
16123            body[1].get_comments()
16124        );
16125    }
16126}